diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba1722be..63fd99d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -141,6 +141,11 @@ jobs: OpenWorkGraph is local-first, open-source context infrastructure for how human and AI-agent work actually happens. + ### First useful reconstruction + The local dashboard now has an evidence-driven first-value layer designed to become useful during the first real work session. It watches only the existing privacy-hardened local evidence surfaces and, once enough activity exists, offers a deterministic **Your last few minutes** reconstruction. This is a presentation layer over canonical evidence, not a new inference or capture pipeline. + + The activation layer adds no sensor, OS/browser permission, AI permission, retention permission, Gateway sharing, MCP tool, screenshot capture, filesystem watcher, prompt/response capture or content telemetry. It never auto-enables AI access or saved history. Empty first-run placeholders are suppressed until their underlying features have useful data, while Evidence, History, Agents, Connect, Organization and Export remain available unchanged. + ### Local-first remains the default A normal OpenWorkGraph install captures to local SQLite, provides local dashboard/export/API/MCP access, and requires no OpenWorkGraph account or OpenWorkGraph-hosted evidence store. Capture continues locally if an optional customer-controlled Gateway or network is unavailable. @@ -156,7 +161,7 @@ jobs: The dashboard now separates giving an AI access to OpenWorkGraph context from observing an agent's own execution. It provides reviewable setup material for Claude Code lifecycle hooks, Codex trace export, OpenAI Agents tracing and generic OpenTelemetry/custom structural adapters. OpenWorkGraph does not silently edit third-party configuration files. An integration is shown as active only when telemetry actually observed by the local evidence store supports that status. ### Custom harnesses - Arbitrary self-built or third-party agent harnesses can now connect in either or both directions. Python and Node/TypeScript helpers, OTLP/HTTP JSON and raw structural HTTP can send privacy-safe execution telemetry through the dedicated write-only agent credential. Any MCP-capable harness can separately read only the OpenWorkGraph context the user has authorized. The setup flow keeps telemetry write permission and context/history read permission explicitly separate. + Arbitrary self-built or third-party agent harnesses can connect in either or both directions. Python and Node/TypeScript helpers, OTLP/HTTP JSON and raw structural HTTP can send privacy-safe execution telemetry through the dedicated write-only agent credential. Any MCP-capable harness can separately read only the OpenWorkGraph context the user has authorized. The setup flow keeps telemetry write permission and context/history read permission explicitly separate. The standalone helpers do not accept or serialize prompt text, model responses, tool arguments/results, returned values, exception text or hidden reasoning. OpenWorkGraph observer failures remain fail-open for the agent. This release publishes **OpenWorkGraph-Agent-Python.py**, **OpenWorkGraph-Agent-Node.mjs** and **OpenWorkGraph-Agent-Node.d.ts** as standalone release assets. diff --git a/VERSION b/VERSION index 5f8cbfdb..13bb6285 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.95.0 +0.96.0 diff --git a/dashboard/first_value_activation.js b/dashboard/first_value_activation.js new file mode 100644 index 00000000..0fcf607e --- /dev/null +++ b/dashboard/first_value_activation.js @@ -0,0 +1,281 @@ +(() => { + 'use strict'; + + const DISMISSED_KEY = 'owg_first_value_dismissed_v1'; + const VIEWED_KEY = 'owg_first_value_reconstruction_viewed_at'; + const POLL_MS = 15000; + const AGENT_POLL_MS = 30000; + let latest = null; + let busy = false; + let agentCache = {executions:[]}; + let agentCacheAt = 0; + + const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + const n = value => Number.isFinite(Number(value)) ? Number(value) : 0; + const safeArray = value => Array.isArray(value) ? value : []; + + function dismissed(){ + try{return localStorage.getItem(DISMISSED_KEY)==='1';}catch(_){return false;} + } + function markDismissed(){ + try{localStorage.setItem(DISMISSED_KEY,'1');}catch(_){} + document.querySelector('#firstValueCard')?.remove(); + } + function milestone(name){ + try{const key=`owg_first_value_${name}_at`;if(!localStorage.getItem(key))localStorage.setItem(key,new Date().toISOString());}catch(_){} + } + function markViewed(){ + try{if(!localStorage.getItem(VIEWED_KEY))localStorage.setItem(VIEWED_KEY,new Date().toISOString());}catch(_){} + } + + async function getJson(url){ + await window.__owgAuthReady; + const response = await fetch(url,{cache:'no-store'}); + if(!response.ok)throw new Error(`GET ${url} failed`); + return response.json(); + } + + function surfaceOf(item){ + const host=String(item?.hostname||'').trim().toLowerCase(); + if(host){ + const parts=host.replace(/^www\./,'').split('.'); + if(parts.length>1)return parts.slice(0,-1).join('.'); + return host; + } + return String(item?.app||'Unknown surface').trim()||'Unknown surface'; + } + + function actionOf(item){ + const label=String(item?.label||'').trim(); + const action=String(item?.action||'').trim().replace(/[_-]+/g,' '); + if(label&&label.toLowerCase()!==surfaceOf(item).toLowerCase())return label.slice(0,120); + return action ? action.charAt(0).toUpperCase()+action.slice(1) : 'Observed activity'; + } + + function recentWindow(summary){ + const rows=safeArray(summary?.recent_evidence).slice().reverse(); + const cutoff=Date.now()-(15*60*1000); + const filtered=rows.filter(row=>{ + const t=Date.parse(String(row?.observed_at||'')); + return !Number.isFinite(t)||t>=cutoff; + }); + return filtered.length?filtered:rows.slice(-30); + } + + function collapsedEvidence(summary){ + const rows=recentWindow(summary); + const output=[]; + for(const row of rows){ + const surface=surfaceOf(row),action=actionOf(row),observed_at=row?.observed_at||''; + const prior=output[output.length-1]; + if(prior&&prior.kind==='human'&&prior.surface===surface&&prior.action===action){ + prior.count+=1;prior.observed_at=observed_at||prior.observed_at;continue; + } + output.push({kind:'human',surface,action,observed_at,count:1,source:String(row?.source||'')}); + } + return output; + } + + function transitionFallback(summary){ + return safeArray(summary?.transitions).slice(0,8).map(row=>({ + kind:'human', + surface:`${String(row?.from||'Work surface')} → ${String(row?.to||'Work surface')}`, + action:`Observed transition${n(row?.count)>1?` · ${n(row?.count)} times`:''}`, + observed_at:'',count:1,source:'summary-transition' + })); + } + + function agentItems(agentPayload){ + const result=[]; + for(const run of safeArray(agentPayload?.executions)){ + const agent=run?.agent||{}; + const name=agent.framework||agent.provider||agent.name||'Agent'; + const operations=run?.operation_counts||{}; + const tools=n(operations.tool_call); + const models=n(operations.model_call); + const approvals=n(run?.approval_request_count); + const failures=n(run?.failure_count)+n(operations.error); + const status=String(run?.outcome_status||'observed'); + const details=[]; + if(models)details.push(`${models} model ${models===1?'call':'calls'}`); + if(tools)details.push(`${tools} tool ${tools===1?'call':'calls'}`); + if(approvals)details.push(`${approvals} approval ${approvals===1?'request':'requests'}`); + if(failures)details.push(`${failures} observed ${failures===1?'failure':'failures'}`); + details.push(status); + result.push({ + kind:'agent',surface:String(name).slice(0,80),action:details.join(' · '), + observed_at:run?.ended_at||run?.started_at||'',count:1, + observation_level:String(run?.observation_level||agent?.observation_level||'partial observation') + }); + } + return result; + } + + function reconstruction(summary,agentPayload){ + let human=collapsedEvidence(summary); + if(!human.length)human=transitionFallback(summary); + const combined=[...human,...agentItems(agentPayload)]; + combined.sort((a,b)=>{ + const ta=Date.parse(String(a.observed_at||'')),tb=Date.parse(String(b.observed_at||'')); + if(!Number.isFinite(ta)&&!Number.isFinite(tb))return 0; + if(!Number.isFinite(ta))return -1;if(!Number.isFinite(tb))return 1;return ta-tb; + }); + const compact=[]; + for(const item of combined){ + const prior=compact[compact.length-1]; + if(prior&&item.kind==='human'&&prior.kind==='human'&&prior.surface===item.surface){ + if(prior.action!==item.action)prior.action=`${prior.action} → ${item.action}`.slice(0,180); + prior.count+=item.count||1;prior.observed_at=item.observed_at||prior.observed_at;continue; + } + compact.push({...item}); + } + return compact.slice(-12); + } + + function stateFrom(summary,agentPayload){ + const evidence=recentWindow(summary),recentSurfaces=new Set(evidence.map(surfaceOf).filter(Boolean)); + const summarySurfaceCount=safeArray(summary?.surfaces).length; + let recentTransitions=0,last=''; + for(const row of evidence){const s=surfaceOf(row);if(last&&s&&s!==last)recentTransitions+=1;if(s)last=s;} + const summarizedTransitions=safeArray(summary?.transitions).length; + const surfaceCount=Math.max(recentSurfaces.size,summarySurfaceCount); + const transitions=Math.max(recentTransitions,summarizedTransitions); + const totalEvents=Math.max(evidence.length,n(summary?.events)); + const runs=safeArray(agentPayload?.executions); + const agentEvents=runs.reduce((total,run)=>total+n(run?.event_count_total),0); + const ready=(totalEvents>=8&&surfaceCount>=2)||(transitions>=2&&totalEvents>=5)||agentEvents>=3; + const apps=safeArray(summary?.apps).map(x=>String(x?.app||'').toLowerCase()); + const browserHeavy=apps.some(x=>/(chrome|edge|safari|firefox|arc|brave)/.test(x)); + const browserConnected=Boolean(summary?.browser_sensor?.status==='connected'||summary?.browser_sensor?.connected||summary?.browser_sensor?.paired||summary?.browser_sensor?.active); + const shallowAgent=runs.some(run=>['os_observed','outcome_only'].includes(String(run?.observation_level||run?.agent?.observation_level||''))); + return {evidence_count:totalEvents,surface_count:surfaceCount,transitions,agent_runs:runs.length,agent_events:agentEvents,ready,browserHeavy,browserConnected,shallowAgent}; + } + + function aiEnabled(payload){return Boolean(payload?.enabled??payload?.ai_access_enabled??payload?.access?.enabled);} + + function ensureStyle(){ + if(document.querySelector('#first-value-style'))return; + const style=document.createElement('style');style.id='first-value-style'; + style.textContent=` + .first-value-card{border:2px solid #285e42;background:linear-gradient(135deg,#ffffff,#f5faf6)} + .first-value-head{display:flex;justify-content:space-between;gap:14px;align-items:flex-start}.first-value-head button{min-height:32px;padding:4px 8px} + .first-value-progress{display:flex;gap:8px;flex-wrap:wrap;margin:13px 0}.first-value-progress span{border:1px solid #d8e5dc;background:#fff;border-radius:999px;padding:6px 9px;font-size:12px} + .first-value-ready{padding:10px 12px;border-radius:11px;background:#edf7f0;color:#285e42;font-weight:750;margin:10px 0} + .first-value-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.first-value-actions button{min-height:38px} + .first-value-trace{margin:10px 0 0;padding:0;list-style:none}.first-value-trace li{display:grid;grid-template-columns:18px minmax(0,1fr);gap:8px;padding:8px 0;border-bottom:1px solid #eceee8}.first-value-trace li:last-child{border-bottom:0}.first-value-node{width:9px;height:9px;border-radius:50%;background:#285e42;margin-top:5px}.first-value-node.agent{background:#3159a5}.first-value-trace strong{font-size:13px}.first-value-trace .muted{margin-top:2px} + `; + document.head.appendChild(style); + } + + function ensureCard(){ + if(dismissed())return null; + const overview=document.querySelector('#panel-overview');if(!overview)return null; + let card=document.querySelector('#firstValueCard'); + if(card)return card; + card=document.createElement('div');card.id='firstValueCard';card.className='card first-value-card'; + card.innerHTML=`
FIRST VALUE

See what OpenWorkGraph understands

Keep working normally. OpenWorkGraph will use the evidence already being captured on this computer to show you a factual reconstruction.
Waiting for enough observed activity to form a useful reconstruction.
`; + const onboarding=document.querySelector('#historyOnboarding'); + if(onboarding&&onboarding.parentNode===overview)onboarding.insertAdjacentElement('afterend',card);else overview.insertBefore(card,overview.firstChild); + card.querySelector('#firstValueDismiss').onclick=markDismissed; + return card; + } + + function hideStaticTimelinePlaceholder(){ + const overview=document.querySelector('#panel-overview');if(!overview)return; + for(const card of overview.querySelectorAll('.card')){ + const text=card.textContent||''; + if(text.includes('Timeline lanes will activate in the stacked capture/timeline PR. Existing evidence collection is unchanged.'))card.style.display='none'; + } + } + + function hideEmptyPlaceholders(summary){ + hideStaticTimelinePlaceholder(); + const patternList=document.querySelector('#patternList'); + if(patternList){ + const card=patternList.closest('.card'); + if(card)card.style.display=n(summary?.repeated_task_pattern_count)>0?'':'none'; + } + } + + function render(state,summary,agentPayload,aiPayload){ + hideEmptyPlaceholders(summary); + if(state.evidence_count>0)milestone('first_event'); + if(state.surface_count>1)milestone('first_cross_surface'); + if(state.agent_runs>0)milestone('first_agent_run'); + if(state.ready)milestone('ready'); + const card=ensureCard();if(!card)return; + card.querySelector('#firstValueProgress').innerHTML=`${state.evidence_count} observed events${state.surface_count} work ${state.surface_count===1?'surface':'surfaces'}${state.transitions} observed ${state.transitions===1?'transition':'transitions'}${state.agent_runs} agent ${state.agent_runs===1?'run':'runs'}`; + const status=card.querySelector('#firstValueStatus'),actions=card.querySelector('#firstValueActions'); + if(state.ready){ + status.className='first-value-ready';status.textContent='OpenWorkGraph has enough observed activity to show a reconstruction. No AI interpretation is required for this view.'; + actions.innerHTML=''; + card.querySelector('#firstValueSee').onclick=()=>openReconstruction(summary,agentPayload,aiPayload,state); + }else{ + status.className='note'; + status.textContent=state.evidence_count===0?'No activity from this run has reached the local evidence store yet. Keep working normally.':'Evidence is arriving. A few more actions or a cross-tool transition will make the reconstruction more useful.'; + actions.innerHTML=''; + card.querySelector('#firstValueEvidence').onclick=()=>window.activateTab?.('evidence'); + } + } + + function traceHtml(items){ + if(!items.length)return '
There is not enough bounded recent evidence to render a trace yet.
'; + return `
    ${items.map(item=>`
  1. ${esc(item.surface)}
    ${esc(item.action)}${item.count>1?` · ${item.count} observations`:''}${item.kind==='agent'?` · ${esc(item.observation_level)}`:''}
  2. `).join('')}
`; + } + + function openReconstruction(summary,agentPayload,aiPayload,state){ + markViewed(); + const items=reconstruction(summary,agentPayload),enabled=aiEnabled(aiPayload); + const prompt='Using OpenWorkGraph, reconstruct what I was doing during the last 10 minutes. Distinguish observed facts from inference.'; + let next=''; + if(enabled){ + next=`

Let your AI inspect the same work

AI access is currently on. Ask it to use OpenWorkGraph rather than relying on chat context alone.
${esc(prompt)}
`; + }else{ + next='

Let your AI understand this too

AI access is still off. Connecting an AI is optional and does not change capture or retention.
'; + } + if(state.browserHeavy&&!state.browserConnected)next+='
Browser context can be richer. Your work includes a browser, but no active browser sensor was detected. The browser sensor is optional.
'; + if(state.shallowAgent)next+='
Agent internals are only partially observed. You can add native/OTel telemetry for deeper structural traces without capturing prompts or responses.
'; + const body=`
Observed evidence only. This reconstruction is assembled from the current session's existing privacy-hardened evidence. It does not infer intent or read hidden reasoning.
${traceHtml(items)}${next}`; + if(typeof window.openModal==='function')window.openModal('Your last few minutes','Observed reconstruction',body);else alert(items.map(x=>`${x.surface}: ${x.action}`).join('\n')); + setTimeout(()=>{ + const copy=document.querySelector('#firstValueCopyPrompt');if(copy)copy.onclick=async()=>{try{await navigator.clipboard.writeText(prompt);copy.textContent='Copied';}catch(_){window.prompt('Copy this:',prompt);}}; + const connect=document.querySelector('#firstValueConnectAI');if(connect)connect.onclick=()=>{window.closeModal?.();window.activateTab?.('connect');}; + const browser=document.querySelector('#firstValueBrowser');if(browser)browser.onclick=()=>{window.closeModal?.();window.activateTab?.('organization');}; + const agent=document.querySelector('#firstValueAgent');if(agent)agent.onclick=()=>{window.closeModal?.();window.activateTab?.('connect');setTimeout(()=>document.querySelector('#agent-observation-setup')?.scrollIntoView({behavior:'smooth',block:'start'}),0);}; + },0); + } + + function shouldPoll(force){ + if(dismissed())return false; + if(force)return true; + const overview=document.querySelector('#tab-overview'); + return !document.hidden&&(!overview||overview.getAttribute('aria-selected')==='true'); + } + + async function refresh(force=false){ + if(busy||!shouldPoll(force))return;busy=true; + try{ + const now=Date.now(); + const agentPromise=(now-agentCacheAt>=AGENT_POLL_MS) + ? getJson('/v1/agent-execution-traces?limit=10&evidence_limit=3000&max_events_per_execution=20') + : Promise.resolve(agentCache); + const [summaryResult,agentResult,aiResult]=await Promise.allSettled([ + getJson('/v1/summary?scope=current&limit=500'),agentPromise,getJson('/v1/ai-access') + ]); + if(summaryResult.status!=='fulfilled')return; + if(agentResult.status==='fulfilled'){agentCache=agentResult.value;agentCacheAt=now;} + const summary=summaryResult.value,agentPayload=agentCache,aiPayload=aiResult.status==='fulfilled'?aiResult.value:{}; + const state=stateFrom(summary,agentPayload);latest={summary,agentPayload,aiPayload,state};render(state,summary,agentPayload,aiPayload); + }finally{busy=false;} + } + + function install(){ + ensureStyle();hideStaticTimelinePlaceholder();refresh(true); + document.querySelector('#tab-overview')?.addEventListener('click',()=>setTimeout(()=>refresh(true),0)); + setInterval(()=>refresh(false),POLL_MS); + } + + window.refreshFirstValue=()=>refresh(true); + window.firstValueReconstruction=()=>latest?openReconstruction(latest.summary,latest.agentPayload,latest.aiPayload,latest.state):refresh(true); + if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',install);else install(); +})(); diff --git a/docs/CHANGELOG_V096.md b/docs/CHANGELOG_V096.md new file mode 100644 index 00000000..d6a7cf91 --- /dev/null +++ b/docs/CHANGELOG_V096.md @@ -0,0 +1,22 @@ +# OpenWorkGraph v0.96 — first useful reconstruction + +v0.96 adds a thin first-run activation layer over the existing local evidence stack. It is intended to make OpenWorkGraph understandable during the first real work session without changing what the product captures or what an AI may access. + +## What changes + +- Overview shows a dismissible **See what OpenWorkGraph understands** card. +- Progress is evidence-driven rather than a countdown: observed events, work surfaces, transitions and agent runs. +- Once enough current-session evidence exists, **Your last few minutes** presents a deterministic reconstruction from existing privacy-hardened dashboard evidence and structural agent-run reports. +- If interaction-level evidence is sparse, already-derived observed surface transitions provide a factual fallback rather than inventing task intent. +- The next action is contextual: Connect AI only when the user chooses, optional browser-sensor setup when browser context is shallow, and optional native/OTel agent telemetry when an agent is only surface-observed. +- The old unfinished timeline placeholder and an empty repeated-workflows card are suppressed on first run until they have useful content. Their underlying DOM/data paths remain intact. + +## What does not change + +v0.96 adds no capture sensor, screenshot capture, filesystem watcher, prompt/response capture, clipboard-content capture, browser/OS permission, database schema, retention rule, AI permission, saved-history lease, Gateway behavior, agent-ingest permission, export format or MCP tool. + +The first-value layer performs GET requests only against existing authenticated local endpoints. It cannot enable AI access, save history, synchronize evidence or mutate canonical evidence. + +## Compatibility goal + +Evidence, History, Agents, Connect, Organization and Export remain the established advanced surfaces. Dismissing the first-value card leaves the ordinary dashboard behavior intact. Existing installations and existing MCP configurations keep their prior semantics. diff --git a/mcpb/manifest.json b/mcpb/manifest.json index fb515277..661049db 100644 --- a/mcpb/manifest.json +++ b/mcpb/manifest.json @@ -2,9 +2,9 @@ "manifest_version": "0.3", "name": "openworkgraph-local", "display_name": "OpenWorkGraph", - "version": "0.95.0", + "version": "0.96.0", "description": "Connect Claude Desktop to the compact local OpenWorkGraph context surface.", - "long_description": "Uses the OpenWorkGraph installation already running on this computer. v0.95 adds a framework-neutral custom-harness setup flow plus standalone Python and Node helpers for privacy-safe structural agent telemetry; arbitrary MCP-capable harnesses can separately read authorized OpenWorkGraph context. v0.94 added explicit local history retention, separate saved-history AI access, lightweight history navigation, and structural browser-agent lifecycle observation. Canonical workflow evidence remains primary; Context Pulse provides incremental factual updates. Retained history is separately user-controlled: list_history can navigate saved human and agent sessions only while a time-limited saved-history lease is active. The legacy 24-tool MCP entrypoint remains available for existing configurations while new connections use this compact surface. Prompts, model responses, tool arguments/results, typed text, clipboard contents, exception text, returned values, and hidden reasoning are not captured by the custom agent helpers.", + "long_description": "Uses the OpenWorkGraph installation already running on this computer. v0.96 adds an evidence-driven first-value dashboard layer that reconstructs the current session from existing privacy-hardened evidence without adding sensors, permissions, AI access, retention, or MCP capabilities. v0.95 added a framework-neutral custom-harness setup flow plus standalone Python and Node helpers for privacy-safe structural agent telemetry; arbitrary MCP-capable harnesses can separately read authorized OpenWorkGraph context. v0.94 added explicit local history retention, separate saved-history AI access, lightweight history navigation, and structural browser-agent lifecycle observation. Canonical workflow evidence remains primary; Context Pulse provides incremental factual updates. Retained history is separately user-controlled: list_history can navigate saved human and agent sessions only while a time-limited saved-history lease is active. The legacy 24-tool MCP entrypoint remains available for existing configurations while new connections use this compact surface. Prompts, model responses, tool arguments/results, typed text, clipboard contents, exception text, returned values, and hidden reasoning are not captured by the custom agent helpers.", "author": {"name": "Koyar Afrasyab / Kinvectum"}, "repository": {"type": "git", "url": "https://github.com/KAVentures/openworkgraph"}, "server": {"type": "node", "entry_point": "server/index.js", "mcp_config": {"command": "node", "args": ["${__dirname}/server/index.js"], "env": {}}}, diff --git a/pyproject.toml b/pyproject.toml index ea4d63cb..293e28c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "workflow-observer" -version = "0.95.0" +version = "0.96.0" description = "Local-first work evidence, self-hosted organizational context gateway, REST API, and MCP access." requires-python = ">=3.11" license = {file = "LICENSE"} diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 0e39a70d..f077d263 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openworkgraph-agent" -version = "0.95.0" +version = "0.96.0" description = "Dependency-free structural telemetry helper for custom OpenWorkGraph agent harnesses" requires-python = ">=3.10" license = {text = "Apache-2.0"} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 8efd3b82..ab9db49d 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@openworkgraph/agent", - "version": "0.95.0", + "version": "0.96.0", "description": "Dependency-free structural telemetry helper for custom OpenWorkGraph agent harnesses", "type": "module", "exports": { diff --git a/server/enterprise_runner.py b/server/enterprise_runner.py index 11a70392..a96f36ae 100644 --- a/server/enterprise_runner.py +++ b/server/enterprise_runner.py @@ -25,6 +25,7 @@ def main() -> None: import server.browser_agent_projection # noqa: F401 import server.agent_dashboard_control_plane # noqa: F401 import server.custom_harness_control_plane # noqa: F401 + import server.first_value_activation # noqa: F401 import server.org_join_routes as org_join_routes import server.dashboard_privacy # noqa: F401 diff --git a/server/first_value_activation.py b/server/first_value_activation.py new file mode 100644 index 00000000..b24d2832 --- /dev/null +++ b/server/first_value_activation.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from fastapi import Request, Response +from fastapi.responses import HTMLResponse + +from .main import ROOT +from .secure_app import app + + +SCRIPT_TAG = '' + + +@app.get("/first-value-activation.js", include_in_schema=False) +def first_value_activation_javascript() -> Response: + """Serve the additive first-value dashboard layer. + + The script reads only existing authenticated local endpoints. It adds no + capture sensor, data store, AI permission, retention permission, or MCP + capability. + """ + path = ROOT / "dashboard" / "first_value_activation.js" + return Response( + path.read_text(encoding="utf-8"), + media_type="application/javascript", + headers={"Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"}, + ) + + +@app.middleware("http") +async def inject_first_value_activation(request: Request, call_next): + response = await call_next(request) + if request.method.upper() != "GET" or request.url.path != "/" or response.status_code != 200: + return response + if "text/html" not in str(response.headers.get("content-type") or "").lower(): + return response + try: + if hasattr(response, "body_iterator"): + chunks = [chunk async for chunk in response.body_iterator] + body = b"".join( + chunk if isinstance(chunk, bytes) else str(chunk).encode("utf-8") + for chunk in chunks + ) + else: + body = bytes(getattr(response, "body", b"")) + text = body.decode("utf-8") + except Exception: + return response + if SCRIPT_TAG not in text: + text = text.replace("", SCRIPT_TAG + "\n") + headers = dict(response.headers) + headers.pop("content-length", None) + return HTMLResponse(text, status_code=response.status_code, headers=headers) + + +__all__ = ["first_value_activation_javascript"] diff --git a/tests/js/first_value_activation.test.mjs b/tests/js/first_value_activation.test.mjs new file mode 100644 index 00000000..77f27608 --- /dev/null +++ b/tests/js/first_value_activation.test.mjs @@ -0,0 +1,34 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +const source = fs.readFileSync(path.resolve('dashboard/first_value_activation.js'), 'utf8'); + +test('first-value activation JavaScript parses standalone', () => { + assert.doesNotThrow(() => new Function(source)); +}); + +test('first-value activation is read-only and evidence driven', () => { + assert.match(source, /\/v1\/summary\?scope=current&limit=500/); + assert.match(source, /\/v1\/agent-execution-traces\?/); + assert.match(source, /\/v1\/ai-access/); + assert.match(source, /state\.ready/); + assert.match(source, /Observed evidence only/); + assert.doesNotMatch(source, /method:\s*['"](?:POST|PUT|PATCH|DELETE)['"]/i); +}); + +test('first-value activation never captures content or hidden reasoning', () => { + for (const forbidden of [ + 'getDisplayMedia(', 'getUserMedia(', 'clipboard.read(', 'clipboard.readText(', + '/agent-ingest/', '/v1/events', '/v1/context-events' + ]) assert.equal(source.includes(forbidden), false, forbidden); + assert.match(source, /does not infer intent or read hidden reasoning/); +}); + +test('first-value guide is dismissible and does not trap advanced navigation', () => { + assert.match(source, /owg_first_value_dismissed_v1/); + assert.match(source, /window\.activateTab\?\.\('evidence'\)/); + assert.match(source, /window\.activateTab\?\.\('connect'\)/); + assert.match(source, /window\.activateTab\?\.\('organization'\)/); +}); diff --git a/tests/test_first_value_activation_v096.py b/tests/test_first_value_activation_v096.py new file mode 100644 index 00000000..7e7af054 --- /dev/null +++ b/tests/test_first_value_activation_v096.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +JS = ROOT / "dashboard" / "first_value_activation.js" +SERVER = ROOT / "server" / "first_value_activation.py" +RUNNER = ROOT / "server" / "enterprise_runner.py" + + +def test_first_value_layer_is_additive_and_loaded_by_desktop_runner(): + server = SERVER.read_text(encoding="utf-8") + runner = RUNNER.read_text(encoding="utf-8") + assert 'from .secure_app import app' in server + assert '/first-value-activation.js' in server + assert 'server.first_value_activation' in runner + assert 'SCRIPT_TAG + "\\n"' in server + + +def test_activation_layer_reads_existing_surfaces_only(): + js = JS.read_text(encoding="utf-8") + for endpoint in ( + "/v1/summary?scope=current&limit=500", + "/v1/agent-execution-traces?limit=10&evidence_limit=3000&max_events_per_execution=20", + "/v1/ai-access", + ): + assert endpoint in js + # First-value activation must not become a second control plane. All writes + # remain behind the established History/Connections/Organization surfaces. + for mutation in ("method:'POST'", 'method:"POST"', "method:'PUT'", 'method:"PUT"', "method:'DELETE'", 'method:"DELETE"'): + assert mutation not in js + assert "fetch(url,{cache:'no-store'})" in js + + +def test_activation_layer_adds_no_capture_or_content_sensor(): + js = JS.read_text(encoding="utf-8").lower() + forbidden_apis = ( + "getdisplaymedia(", + "getusermedia(", + "filesystemobserver", + "clipboard.read", + "clipboard.readtext", + "mutationobserver(", + ) + for marker in forbidden_apis: + assert marker not in js + # The layer may explain these privacy boundaries in UI copy, but it must not + # define content-bearing telemetry fields or write them to a new endpoint. + assert "/agent-ingest/" not in js + assert "/v1/events" not in js + assert "/v1/context-events" not in js + + +def test_activation_never_auto_enables_ai_or_retention(): + js = JS.read_text(encoding="utf-8") + assert "/v1/history-policy" not in js + assert "/v1/history/ai-access" not in js + assert "set_ai_access" not in js + assert "Connect AI" in js + assert "Connecting an AI is optional and does not change capture or retention" in js + + +def test_existing_mcp_and_browser_permissions_are_untouched_by_activation_layer(): + # These files are read, not modified, by this feature. The assertions make + # the intended v0.96 boundary explicit so a future activation edit cannot + # quietly grow browser privileges or replace MCP with an onboarding API. + manifest = (ROOT / "browser_extension" / "manifest.json").read_text(encoding="utf-8") + assert '"permissions": ["tabs", "webNavigation", "storage", "alarms"]' in manifest + assert "first_value_activation" not in manifest + compact = (ROOT / "mcp_server" / "compact_stdio.py").read_text(encoding="utf-8") + legacy = (ROOT / "mcp_server" / "secure_stdio.py").read_text(encoding="utf-8") + assert "first_value_activation" not in compact + assert "first_value_activation" not in legacy + + +def test_stale_first_run_placeholders_are_hidden_without_deleting_their_dom(): + js = JS.read_text(encoding="utf-8") + assert "Timeline lanes will activate in the stacked capture/timeline PR" in js + assert "card.style.display='none'" in js + assert "repeated_task_pattern_count" in js + # The base dashboard remains the source of the advanced interfaces; this + # layer only changes first-run presentation at runtime. + html = (ROOT / "dashboard" / "index.html").read_text(encoding="utf-8") + for tab in ("overview", "evidence", "connect", "organization", "export"): + assert f'data-tab="{tab}"' in html diff --git a/tests/test_release_version_v087.py b/tests/test_release_version_v087.py index e1130288..2d8e6a7c 100644 --- a/tests/test_release_version_v087.py +++ b/tests/test_release_version_v087.py @@ -6,7 +6,7 @@ ROOT = Path(__file__).resolve().parents[1] -EXPECTED_VERSION = "0.95.0" +EXPECTED_VERSION = "0.96.0" def test_release_version_sources_are_aligned(): @@ -40,6 +40,7 @@ def test_release_notes_are_current_and_version_driven(): assert "Agent setup control plane" in workflow assert "telemetry actually observed" in workflow assert "Custom harnesses" in workflow + assert "First useful reconstruction" in workflow assert "pkg-agent-sdk" in workflow assert "OpenWorkGraph-Agent-Python.py" in workflow assert "OpenWorkGraph-Agent-Node.mjs" in workflow