Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bd47ceb
Add privacy-safe Claude Code OTel event adapter
KAVentures Sep 26, 2026
791b017
Ingest Claude Code structural OTel events
KAVentures Sep 26, 2026
4080d86
Expose write-only Claude Code OTel ingest endpoint
KAVentures Sep 26, 2026
524245b
Correlate Claude hooks by prompt and observe subagent handoffs
KAVentures Sep 26, 2026
36f5f21
Add capability-aware agent observability summaries
KAVentures Sep 26, 2026
f201a26
Expose signal capability and rich run summaries
KAVentures Sep 26, 2026
9a04c14
Enrich Codex turn, usage and multi-agent telemetry
KAVentures Sep 26, 2026
0dc10c1
Carry rich agent observability onto post-#86 base
KAVentures Sep 26, 2026
1f42c11
Add privacy-safe Claude Code OTel configuration helper
KAVentures Sep 26, 2026
aa36447
Make Claude one-click setup manage rich telemetry safely
KAVentures Sep 26, 2026
8208aa8
Wire rich Claude telemetry into one-click agent setup
KAVentures Sep 26, 2026
4cca0db
Make agent dashboard capability-aware instead of showing fake zeroes
KAVentures Sep 26, 2026
1bce005
Enrich OpenAI Agents response usage and telemetry identity
KAVentures Sep 26, 2026
36f9e1d
Update Claude hook tests for turn correlation and handoff edges
KAVentures Sep 26, 2026
bd26f0c
Test rich Claude, Codex, OpenAI and capability-aware observability
KAVentures Sep 26, 2026
b913408
Test safe rich Claude one-click telemetry configuration
KAVentures Sep 26, 2026
8463348
Test capability-aware agent dashboard semantics
KAVentures Sep 26, 2026
179d1c6
Bump rich agent observability release to v0.90.0
KAVentures Sep 26, 2026
fe295df
Bump package version to v0.90.0
KAVentures Sep 26, 2026
3dabe59
Bump MCPB version to v0.90.0
KAVentures Sep 26, 2026
abee536
Document rich capability-aware native agent observation
KAVentures Sep 26, 2026
ee4c4d7
Test Claude OTel route auth and privacy
KAVentures Sep 26, 2026
47e21b7
Expose rich capability-aware agent summaries through MCP
KAVentures Sep 26, 2026
6bf0dad
Fix optional Claude SessionStart model label
KAVentures Sep 26, 2026
863955e
Update Claude label privacy test for explicit handoff plus child run
KAVentures Sep 26, 2026
9633292
Advance release-version test to v0.90.0
KAVentures Sep 26, 2026
61f404e
Preserve exact generic OTel transport guidance
KAVentures Sep 26, 2026
232d44f
Fix privacy assertion to test values rather than canonical field names
KAVentures Sep 26, 2026
b341ce1
Export Codex structural logs as well as traces
KAVentures Sep 26, 2026
caf8371
Describe rich Claude and Codex telemetry accurately in setup UI
KAVentures Sep 26, 2026
49bbcbc
Advertise Codex structural logs plus traces in agent setup
KAVentures Sep 26, 2026
e398280
Document Codex structural logs and trace export
KAVentures Sep 26, 2026
fcd1545
Verify Codex one-click exports structural logs and traces
KAVentures Sep 26, 2026
e36cc14
Detect and upgrade older trace-only Codex OWG setup
KAVentures Sep 26, 2026
3c8e77c
Test upgrade from older trace-only Codex managed setup
KAVentures Sep 26, 2026
e061d5f
Update Codex config test for structural logs and traces
KAVentures Sep 27, 2026
4e93b2e
Fix cross-platform file mode assertion on Windows
KAVentures Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.89.0
0.90.0
29 changes: 29 additions & 0 deletions adapters/claude_code_otel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import annotations

"""Build the Claude Code settings env needed for structural OWG telemetry only."""


def env_settings(*, token: str, base_url: str) -> dict[str, str]:
base = str(base_url or "").rstrip("/")
return {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": "http/json",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": f"{base}/agent-ingest/v1/claude-otel",
"OTEL_EXPORTER_OTLP_LOGS_HEADERS": f"Authorization=Bearer {token}",
# Claude leaves these content surfaces off by default. OWG writes the
# explicit zeroes as defense in depth; the ingest adapter independently
# allowlists structural attributes and would discard content anyway.
"OTEL_LOG_USER_PROMPTS": "0",
"OTEL_LOG_ASSISTANT_RESPONSES": "0",
"OTEL_LOG_TOOL_DETAILS": "0",
"OTEL_LOG_TOOL_CONTENT": "0",
"OTEL_LOG_RAW_API_BODIES": "0",
}


def settings_fragment(*, token: str, base_url: str) -> dict[str, dict[str, str]]:
return {"env": env_settings(token=token, base_url=base_url)}


__all__ = ["env_settings", "settings_fragment"]
31 changes: 20 additions & 11 deletions adapters/codex_config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

"""Print a Codex OTLP trace-exporter snippet for OpenWorkGraph.
"""Print a privacy-safe Codex OTLP logs + trace exporter snippet for OWG.

This helper never edits ~/.codex/config.toml. With --with-token it intentionally
prints the least-privilege write token so an administrator can paste a complete
local-only configuration.
Codex's business events (API requests, completed tools, approval decisions and
multi-agent communication) are emitted on its OTLP log surface, while native
span hierarchy is emitted on the trace surface. OWG points both at the same
write-only structural endpoint and keeps all content-bearing opt-ins disabled.
"""

import argparse
Expand All @@ -20,21 +21,29 @@ def _toml_string(value: str) -> str:
return json.dumps(value)


def _exporter(endpoint: str, authorization: str) -> str:
return (
"{ otlp-http = { endpoint = "
+ _toml_string(endpoint)
+ ", headers = { Authorization = "
+ _toml_string(authorization)
+ " }, protocol = \"json\" } }"
)


def config_snippet(*, token: str, base_url: str | None = None) -> str:
endpoint = (base_url or _base_url()).rstrip("/") + "/agent-ingest/v1/codex-otel"
authorization = f"Bearer {token}"
exporter = _exporter(endpoint, authorization)
return "\n".join([
"[otel]",
# Keep every Codex content-bearing opt-in disabled. Structural business
# events still export and are then strict-allowlisted again server-side.
"log_user_prompt = false",
"log_agent_responses = false",
"log_guardian_assessments = false",
(
"trace_exporter = { otlp-http = { endpoint = "
+ _toml_string(endpoint)
+ ", headers = { Authorization = "
+ _toml_string(authorization)
+ " }, protocol = \"json\" } }"
),
f"exporter = {exporter}",
f"trace_exporter = {exporter}",
])


Expand Down
42 changes: 35 additions & 7 deletions adapters/openai_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class _TracingProcessor: # type: ignore[no-redef]

_SAFE_LABEL = re.compile(r"^[A-Za-z][A-Za-z0-9_.:/-]{0,199}$")
_USAGE_KEYS = frozenset({"input_tokens", "output_tokens", "cached_input_tokens", "total_tokens"})
_SENSOR_ID = "agent:openai-agents"


def _now_iso() -> str:
Expand Down Expand Up @@ -86,21 +87,34 @@ def _duration_seconds(span: Any) -> float:


def _usage(value: Any) -> dict[str, int]:
if not isinstance(value, dict):
"""Read only numeric token counters from dict- or object-shaped SDK usage."""
if value is None:
return {}
out: dict[str, int] = {}
for key in _USAGE_KEYS:
if key not in value:
if isinstance(value, dict):
raw = value.get(key)
else:
raw = getattr(value, key, None)
if raw is None:
continue
try:
amount = int(value[key])
amount = int(raw)
except Exception:
continue
if 0 <= amount <= 1_000_000_000:
out[key] = amount
if "total_tokens" not in out and ("input_tokens" in out or "output_tokens" in out):
out["total_tokens"] = out.get("input_tokens", 0) + out.get("output_tokens", 0)
return out


def _response_model(data: Any) -> str:
"""Read only the response model identifier, never response content."""
response = getattr(data, "response", None)
return _safe_label(getattr(response, "model", ""), limit=200) if response is not None else ""


def _tool_category(name: str, *, is_mcp: bool = False) -> str:
if is_mcp:
return "mcp"
Expand Down Expand Up @@ -212,6 +226,7 @@ def on_trace_start(self, trace: Any) -> None:
self._emit({
"event_id": _event_id("trace-start", raw_trace),
"observed_at": _now_iso(),
"sensor_id": _SENSOR_ID,
"agent_name": "OpenAI-Agents-SDK",
"provider": "openai",
"framework": "openai-agents-python",
Expand All @@ -232,6 +247,7 @@ def on_trace_end(self, trace: Any) -> None:
self._emit({
"event_id": _event_id("trace-end", raw_trace),
"observed_at": _now_iso(),
"sensor_id": _SENSOR_ID,
"agent_name": "OpenAI-Agents-SDK",
"provider": "openai",
"framework": "openai-agents-python",
Expand Down Expand Up @@ -271,6 +287,7 @@ def on_span_end(self, span: Any) -> None:
base: dict[str, Any] = {
"event_id": _event_id(span_type, raw_trace, raw_span),
"observed_at": observed_at,
"sensor_id": _SENSOR_ID,
"agent_name": agent_name,
"provider": "openai",
"framework": "openai-agents-python",
Expand All @@ -285,10 +302,21 @@ def on_span_end(self, span: Any) -> None:

if span_type in {"generation", "response", "transcription", "speech"}:
base["operation"] = "model_call"
if span_type != "response":
base["model"] = _safe_label(getattr(data, "model", ""), limit=200)
if span_type == "generation":
base["usage"] = _usage(getattr(data, "usage", None))
if span_type == "response":
model = _response_model(data)
if model:
base["model"] = model
usage = _usage(getattr(data, "usage", None))
if usage:
base["usage"] = usage
else:
model = _safe_label(getattr(data, "model", ""), limit=200)
if model:
base["model"] = model
if span_type == "generation":
usage = _usage(getattr(data, "usage", None))
if usage:
base["usage"] = usage
self._emit(base)
return

Expand Down
18 changes: 9 additions & 9 deletions dashboard/agent_control_plane.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@
const section=document.createElement('div');
section.id='agent-observation-setup';section.className='card connection-section';
section.innerHTML=`
<div class="agent-control-intro"><div><h2>Observe an agent</h2><div class="muted">Instrument an agent's native lifecycle or trace surface so OpenWorkGraph can measure structural execution: runs, tools, handoffs, approvals, failures, timings and coverage. Prompts, responses, reasoning, tool arguments and tool results are not collected.</div></div><button id="refreshAgentConnections" class="secondary" type="button">Check telemetry</button></div>
<div class="agent-control-intro"><div><h2>Observe an agent</h2><div class="muted">Instrument an agent's native lifecycle or telemetry surface so OpenWorkGraph can measure structural execution: runs, models, tools, handoffs, approvals, failures, timings and coverage. Prompts, responses, reasoning, tool arguments and tool results are not collected.</div></div><button id="refreshAgentConnections" class="secondary" type="button">Check telemetry</button></div>
<div class="note" style="margin-top:12px"><strong>Status means telemetry observed.</strong> The green badge appears only when evidence actually arrives. <strong>Connect</strong> adds OpenWorkGraph's entries to that agent's own settings file (a backup is written first and your other settings are kept); <strong>Disconnect</strong> removes only what OpenWorkGraph added.</div>
<div class="setup-grid" style="margin-top:12px">
${setupCard('claude_code','Native hooks','Claude Code','Observe lifecycle, tool use, failures, approval requests and subagent handoffs without capturing prompts or tool contents.')}
${setupCard('codex','OTel trace','Codex','Use Codex trace export only. OpenWorkGraph does not enable the richer diagnostic log stream.')}
${setupCard('openai_agents','Tracing processor','OpenAI Agents SDK','Register OpenWorkGraph as an additional tracing processor; existing SDK tracing remains active.')}
${setupCard('otel','Provider-neutral','OpenTelemetry / custom','Send OTLP/HTTP JSON traces or canonical structural events from another agent runtime.')}
${setupCard('claude_code','Hooks + OTel logs','Claude Code','Observe per-request model calls and tokens, tool use, approval requests/decisions, failures and subagent handoffs without capturing prompt or tool content.')}
${setupCard('codex','OTel logs + trace','Codex','Observe structural API, tool, approval and multi-agent events plus trace hierarchy. Content-bearing log options stay disabled.')}
${setupCard('openai_agents','Tracing processor','OpenAI Agents SDK','Register OpenWorkGraph as an additional tracing processor for model, tool, handoff, hierarchy, usage and timing signals.')}
${setupCard('otel','Provider-neutral','OpenTelemetry / custom','Send portable GenAI OTLP/HTTP JSON traces or canonical structural events from another agent runtime.')}
</div>`;
const anchor=grid||panel.lastElementChild;
if(anchor&&anchor.parentNode===panel)anchor.insertAdjacentElement('afterend',section);else panel.appendChild(section);
Expand Down Expand Up @@ -114,7 +114,7 @@
renderConfigState();
const where=result.path?`<code>${h(result.path)}</code>`:'its settings file';
const backup=result.backup?`<div class="note">Backup of the previous file: <code>${h(result.backup)}</code></div>`:'';
if(action==='connect')window.openModal?.(`${label} connected`,'Agent observation',`<p>OpenWorkGraph's observation hooks were added to ${where}. ${h(result.note||'')}</p>${backup}<div class="note">The status turns green once the first telemetry arrives. Disconnect removes only OpenWorkGraph's entries.</div>`);
if(action==='connect')window.openModal?.(`${label} connected`,'Agent observation',`<p>OpenWorkGraph's observation settings were added to ${where}. ${h(result.note||'')}</p>${backup}<div class="note">The status turns green once the first telemetry arrives. Disconnect removes only OpenWorkGraph's entries.</div>`);
else window.openModal?.(`${label} disconnected`,'Agent observation',`<p>OpenWorkGraph's entries were removed from ${where}. Your other settings were left as they were.</p>${backup}`);
}catch(_){
renderConfigState();
Expand Down Expand Up @@ -156,10 +156,10 @@
let title='Observe an agent',body='';
if(kind==='claude_code'){
const x=integrations.claude_code||{};title='Observe Claude Code';
body=`<p>Prefer the <strong>Connect</strong> button, which does this for you. To do it by hand, merge the <code>hooks</code> object below into your Claude Code settings.</p>${privacyHtml(payload)}<h3>Dashboard-generated settings</h3>${codeBox(JSON.stringify(x.settings||{},null,2),'agentSetupCode')}<h3 style="margin-top:16px">Equivalent command</h3>${codeBox(x.command||'python -m adapters.claude_code_hook --print-settings','agentSetupCommand')}<div class="note">Hooks are asynchronous and fail-open. If OpenWorkGraph is unavailable, Claude Code continues normally.</div>`;
body=`<p>Prefer the <strong>Connect</strong> button, which does this for you. To do it by hand, merge the <code>hooks</code> and <code>env</code> objects below into your Claude Code settings.</p>${privacyHtml(payload)}<h3>Dashboard-generated settings</h3>${codeBox(JSON.stringify(x.settings||{},null,2),'agentSetupCode')}<h3 style="margin-top:16px">Equivalent hook command</h3>${codeBox(x.command||'python -m adapters.claude_code_hook --print-settings','agentSetupCommand')}<div class="note">Hooks are asynchronous and fail-open. OTel content logging is explicitly disabled; the OWG ingest path independently strict-allowlists structural fields.</div>`;
}else if(kind==='codex'){
const x=integrations.codex||{};title='Observe Codex';
body=`<p>Prefer the <strong>Connect</strong> button, which adds this for you unless you already have your own <code>[otel]</code> settings. To do it by hand, merge these keys into your existing <code>[otel]</code> section. The dashboard includes only the dedicated local write-only telemetry credential; it does not grant access to your OWG history.</p>${privacyHtml(payload)}${codeBox(x.config||'', 'agentSetupCode')}<div class="note">OpenWorkGraph enables trace export only. User prompts, agent responses and guardian assessments remain disabled.</div>`;
body=`<p>Prefer the <strong>Connect</strong> button, which adds this for you unless you already have your own <code>[otel]</code> settings. To do it by hand, merge these keys into your existing <code>[otel]</code> section. The dashboard includes only the dedicated local write-only telemetry credential; it does not grant access to your OWG history.</p>${privacyHtml(payload)}${codeBox(x.config||'', 'agentSetupCode')}<div class="note">OpenWorkGraph enables structural Codex log events and trace export. User prompts, agent responses and guardian assessments remain disabled, and content-bearing fields are discarded server-side.</div>`;
}else if(kind==='openai_agents'){
const x=integrations.openai_agents||{};title='Observe OpenAI Agents SDK';
body=`<p>Add OpenWorkGraph as an additional tracing processor in the agent application's Python environment.</p>${privacyHtml(payload)}${codeBox(x.python||'', 'agentSetupCode')}<div class="note">This does not replace existing SDK tracing and does not make OpenWorkGraph a dependency for the agent's control flow.</div>`;
Expand Down Expand Up @@ -228,4 +228,4 @@
}

if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',install);else install();
})();
})();
Loading
Loading