Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion dashboard/agent_control_plane.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@
if(!panel||document.querySelector('#agent-control-intro'))return;
const intro=document.createElement('div');
intro.id='agent-control-intro';intro.className='card';
intro.innerHTML=`<div class="agent-control-intro"><div><h2>Agent observation</h2><div class="muted">Runs appear below when a native hook, tracing processor, OpenTelemetry exporter or custom structural adapter actually sends evidence to this local observer. MCP context access and agent observation are separate connections.</div></div><button id="openAgentSetup" type="button">Set up an agent</button></div>`;
intro.innerHTML=`<div class="agent-control-intro"><div><h2>Agent observation</h2><div class="muted">Runs appear below when a native hook, tracing processor, OpenTelemetry exporter or custom structural adapter actually sends evidence to this local observer. MCP context access and agent observation are separate connections.</div><label class="muted" style="display:flex;gap:6px;align-items:center;margin-top:8px;font-size:12.5px"><input type="checkbox" id="showDisconnectedAgents"> Show past runs from agents whose Observe is off</label><div class="muted" id="agentHiddenNote" style="font-size:12px;margin-top:4px"></div></div><button id="openAgentSetup" type="button">Set up an agent</button></div>`;
panel.insertBefore(intro,panel.firstChild);
const history=intro.querySelector('#showDisconnectedAgents');
try{history.checked=localStorage.getItem('owg_show_disconnected_agents')==='1';}catch(_){}
history.onchange=()=>{try{localStorage.setItem('owg_show_disconnected_agents',history.checked?'1':'0');}catch(_){}window.refreshAgentObservability?.(true);window.refreshAgentDashboard?.(true);};
intro.querySelector('#openAgentSetup').onclick=()=>{window.activateTab?.('connect');setTimeout(()=>document.querySelector('#agent-observation-setup')?.scrollIntoView({behavior:'smooth',block:'start'}),0);};
}

Expand Down
14 changes: 13 additions & 1 deletion dashboard/agent_observability_v090.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
(() => {
// Shared with the other Agents-tab script: both must request the same run list.
const agentHistoryParam=()=>{try{return localStorage.getItem('owg_show_disconnected_agents')==='1'?'':'&hide_disconnected=true';}catch(_){return '&hide_disconnected=true';}};
let latest=null;
let loading=false;
let patchScheduled=false;
Expand Down Expand Up @@ -139,9 +141,17 @@
note.textContent=`${base}${base?' ':''}0 = observed zero. — = this integration cannot observe that signal; it does not mean zero runtime activity.`;
}

function patchHiddenNote(){
const note=document.querySelector('#agentHiddenNote');if(!note)return;
const names={'claude-code':'Claude Code','codex':'Codex'};
const hidden=(latest.hidden_frameworks||[]).map(f=>names[f]||f);
note.textContent=hidden.length?`Past runs from ${hidden.join(' and ')} are hidden because Observe is off for ${hidden.length>1?'them':'it'}. Nothing was deleted.`:'';
}

function patch(){
patchScheduled=false;
if(!latest)return;
patchHiddenNote();
const runs=Array.isArray(latest.executions)?latest.executions:[];
patchMetrics(runs);patchReports(runs);patchRunRows(runs);patchNote();
}
Expand All @@ -157,11 +167,13 @@
loading=true;
try{
await window.__owgAuthReady;
const response=await fetch('/v1/agent-execution-traces?limit=50&evidence_limit=25000&max_events_per_execution=100',{cache:'no-store'});
const response=await fetch('/v1/agent-execution-traces?limit=50&evidence_limit=25000&max_events_per_execution=100'+agentHistoryParam(),{cache:'no-store'});
if(response.ok){latest=await response.json();schedulePatch();}
}finally{loading=false;}
}

window.refreshAgentObservability=force=>{latest=null;return refresh(force);};

function install(){
document.querySelector('#tab-agents')?.addEventListener('click',()=>setTimeout(()=>refresh(true),0));
document.querySelector('#agentRefreshButton')?.addEventListener('click',()=>setTimeout(()=>refresh(true),30));
Expand Down
15 changes: 14 additions & 1 deletion dashboard/connections.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
if(!card){
card=document.createElement('div');
card.id='owgConnections';card.className='card';
card.innerHTML=`<div class="conn-head"><div><h2 style="margin-bottom:5px">Connections</h2><div class="muted"><strong>Context</strong> lets an app read the work context you allow. <strong>Observe</strong> lets OpenWorkGraph record how an agent runs (never prompts, responses, tool arguments or results). Flip a switch to turn either on or off. The first time sets the app up (with a backup of its settings); after that, on/off is instant.</div></div><div class="conn-master" id="owgMaster"></div></div><div id="owgConnTable"><div class="muted" style="margin-top:12px">Checking your apps…</div></div><div class="cli"><div style="display:flex;justify-content:space-between;align-items:center;gap:8px"><span><strong>For agents and scripts</strong> <span class="muted">(same switches, JSON output; works from any folder)</span></span><button class="linkish" id="owgCliCopy" type="button">Copy</button></div><code id="owgCli">…</code></div>`;
card.innerHTML=`<div class="conn-head"><div><h2 style="margin-bottom:5px">Connections</h2><div class="muted"><strong>Context</strong> lets an app read the work context you allow. <strong>Observe</strong> lets OpenWorkGraph record how an agent runs (never prompts, responses, tool arguments or results). Flip a switch to turn either on or off. The first time sets the app up (with a backup of its settings); after that, on/off is instant.</div></div><div class="conn-master" id="owgMaster"></div></div><div id="owgConnTable"><div class="muted" style="margin-top:12px">Checking your apps…</div></div><div class="sub" id="owgCloudNote" style="margin-top:12px;font-size:12.5px"><strong>Cloud apps</strong> (ChatGPT, Lovable, Microsoft 365 Copilot) run on their servers, so they can't reach OpenWorkGraph on this computer directly. They need a secure tunnel: <button class="linkish" type="button" onclick="openConnect('chatgpt')">how to connect a cloud app</button></div><div class="cli"><div style="display:flex;justify-content:space-between;align-items:center;gap:8px"><span><strong>For agents and scripts</strong> <span class="muted">(same switches, JSON output; works from any folder)</span></span><button class="linkish" id="owgCliCopy" type="button">Copy</button></div><code id="owgCli">…</code></div>`;
panel.insertBefore(card,panel.firstChild);
}
// Everything the table already covers moves into one collapsed section.
Expand Down Expand Up @@ -81,7 +81,20 @@
return `<div class="cell"><button class="sw" role="switch" aria-checked="${info.on?'true':'false'}" aria-label="${esc(label)}" data-client="${esc(client.id)}" data-kind="${esc(kind)}" ${busy.has(key)?'disabled':''}></button></div>`;
}

function restartLine(client){
// The app loads this config only at startup and has not restarted since.
for(const [kind,label] of [['observe','Observe'],['mcp','Context']]){
const r=client[kind]?.restart_needed;if(!r)continue;
const since=new Date(r.running_since);
const when=Number.isFinite(since.getTime())?since.toLocaleDateString(undefined,{month:'short',day:'numeric'}):'';
const quit=/Mac/i.test(navigator.platform||'')?' (⌘Q)':'';
return `<div class="sub warn">Quit and reopen ${esc(r.app)}${quit} to finish turning on ${label}${when?` (running since ${esc(when)})`:''}</div>`;
}
return '';
}

function subline(client){
const restart=restartLine(client);if(restart)return restart;
const msg=lastMessage[client.id];
if(msg)return `<div class="sub${msg.warn?' warn':''}">${esc(msg.text)}</div>`;
const errors=[client.mcp,client.observe].map(x=>x&&x.error).filter(Boolean);
Expand Down
4 changes: 3 additions & 1 deletion dashboard/v0571_polish.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
(() => {
// Shared with the other Agents-tab script: both must request the same run list.
const agentHistoryParam=()=>{try{return localStorage.getItem('owg_show_disconnected_agents')==='1'?'':'&hide_disconnected=true';}catch(_){return '&hide_disconnected=true';}};
const KNOWN = [
[/mail\.google\.com|\bgmail\b/i, 'Gmail'],
[/docs\.google\.com\/spreadsheets|google sheets|\bsheets\b/i, 'Google Sheets'],
Expand Down Expand Up @@ -275,7 +277,7 @@
agentLoading=true;
try {
await window.__owgAuthReady;
const response=await fetch('/v1/agent-execution-traces?limit=50&evidence_limit=25000&max_events_per_execution=100',{cache:'no-store'});
const response=await fetch('/v1/agent-execution-traces?limit=50&evidence_limit=25000&max_events_per_execution=100'+agentHistoryParam(),{cache:'no-store'});
if (!response.ok) throw new Error('agent traces unavailable');
renderAgentDashboard(await response.json());
} catch (_) {
Expand Down
9 changes: 7 additions & 2 deletions docs/CONNECTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ OpenWorkGraph has one list of AI apps, on the dashboard's **Connect** tab under
| Claude Desktop | ✓ | – | `claude_desktop_config.json` in Claude's app-support folder |
| Codex | ✓ | ✓ OTel traces | `~/.codex/config.toml` (or `$CODEX_HOME`) |
| Cursor | ✓ | – | `~/.cursor/mcp.json` |
| VS Code | ✓ | – | `mcp.json` in VS Code's user folder |
| VS Code + GitHub Copilot | ✓ | – | `mcp.json` in VS Code's user folder (Copilot agent mode uses it) |
| Windsurf | ✓ | – | `~/.codeium/windsurf/mcp_config.json` |
| Gemini CLI | ✓ | – | `~/.gemini/settings.json` |
| GitHub Copilot CLI | ✓ | – | `~/.copilot/mcp-config.json` (or `$COPILOT_HOME`) |
| Kiro | ✓ | – | `~/.kiro/settings/mcp.json` |
| Amazon Q Developer | ✓ | – | `~/.aws/amazonq/mcp.json` |

ChatGPT (cloud), other MCP apps and custom agents (OpenAI Agents SDK, OpenTelemetry, custom events) are under **More ways to connect**.
**Cloud apps** (ChatGPT, Lovable, Microsoft 365 Copilot) run on their own servers and accept only remote HTTP MCP servers, so they cannot reach OpenWorkGraph on this computer directly. They need a secure tunnel to the optional local HTTP endpoint (or an organization Gateway). Other MCP apps and custom agents (OpenAI Agents SDK, OpenTelemetry, custom events) are under **More ways to connect**.

## How the switches behave

Expand All @@ -27,6 +30,8 @@ ChatGPT (cloud), other MCP apps and custom agents (OpenAI Agents SDK, OpenTeleme
- It never duplicates its own entry.
- It refuses (and changes nothing) if the file is not valid JSON/TOML, or if you already have a hand-written entry it should not overwrite.
- **Off / on again.** This only flips a switch that OpenWorkGraph checks on every MCP tool call and every incoming agent event. It takes effect **immediately**, with no restart, and the config stays in place.
- **Restart needed.** Some apps load this config only when they start. The ChatGPT app (Codex engine) does this for Observe, and Claude Desktop does it for Context. If the app has been running since before OpenWorkGraph changed its config, the row says so ("Quit and reopen the ChatGPT app (⌘Q)…"). Opening a new chat is not enough.
- **Agents tab history.** Runs from an agent whose Observe is off or removed are hidden by default. Tick *Show past runs from agents whose Observe is off* to see them. Nothing is deleted.
- **Remove** deletes OpenWorkGraph's entry from the app's config file (with a backup).
- **Context also needs AI access.** The dashboard's **AI access** master switch resets to OFF each time OpenWorkGraph starts. Turning a Context switch on also turns AI access on for the current run.

Expand Down
28 changes: 27 additions & 1 deletion server/agent_execution_trace_routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import json
from typing import Any

from fastapi import APIRouter, HTTPException, Request
Expand All @@ -18,6 +19,19 @@ def _require_agent_report_read(request: Request) -> None:
raise HTTPException(status_code=401, detail="API or dashboard authentication required")


def _row_framework(row: dict[str, Any]) -> str:
if str(row.get("source") or "") != "agent":
return ""
meta = row.get("metadata")
if meta is None and row.get("metadata_json"):
try:
meta = json.loads(row["metadata_json"])
except (TypeError, ValueError):
return ""
agent = meta.get("agent") if isinstance(meta, dict) else None
return str((agent or {}).get("framework") or "") if isinstance(agent, dict) else ""


@router.get("/v1/agent-execution-traces")
def get_agent_execution_traces(
request: Request,
Expand All @@ -27,14 +41,25 @@ def get_agent_execution_traces(
evidence_limit: int = 25_000,
limit: int = 20,
max_events_per_execution: int = 100,
hide_disconnected: bool = False,
) -> dict[str, Any]:
"""Return privacy-safe ordered structural traces for observed agent runs."""
"""Return privacy-safe ordered structural traces for observed agent runs.

``hide_disconnected`` omits stored runs from agents whose Observe connection
is currently off or removed (history is kept, only the view is filtered).
"""
_require_agent_report_read(request)
hidden: set[str] = set()
try:
raw = load_recent_evidence(
limit=max(1, min(int(evidence_limit), 100_000)),
since=since,
)
if hide_disconnected:
from .connections import hidden_frameworks
hidden = hidden_frameworks()
if hidden:
raw = [row for row in raw if _row_framework(row) not in hidden]
payload = agent_execution_traces(
raw,
family_key=family_key,
Expand All @@ -48,6 +73,7 @@ def get_agent_execution_traces(
return {
**payload,
"evidence_rows_considered": len(raw),
"hidden_frameworks": sorted(hidden),
"evidence_is_canonical": True,
"read_only": True,
"writes_performed": False,
Expand Down
Loading
Loading