@aiwg/cockpit 2026.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +337 -0
  2. package/bridge/package.json +13 -0
  3. package/bridge/src/public/index.html +395 -0
  4. package/bridge/src/server.mjs +1132 -0
  5. package/bridge/src/smoke.mjs +158 -0
  6. package/contrib/aiwg-core.json +15 -0
  7. package/contrib/contribution.schema.json +69 -0
  8. package/desktop/README.md +42 -0
  9. package/desktop/src-tauri/Cargo.toml +17 -0
  10. package/desktop/src-tauri/build.rs +3 -0
  11. package/desktop/src-tauri/frontend/index.html +11 -0
  12. package/desktop/src-tauri/src/main.rs +55 -0
  13. package/desktop/src-tauri/tauri.conf.json +20 -0
  14. package/package.json +49 -0
  15. package/runtime-docs/README.md +38 -0
  16. package/shell-core/runtime.mjs +45 -0
  17. package/shell-core/smoke.mjs +36 -0
  18. package/vscode/README.md +25 -0
  19. package/vscode/extension.js +59 -0
  20. package/vscode/package.json +29 -0
  21. package/web/dist/assets/index-B5anpdS1.js +67 -0
  22. package/web/dist/assets/index-CP3BF6uZ.css +32 -0
  23. package/web/dist/index.html +14 -0
  24. package/web/index.html +13 -0
  25. package/web/package.json +31 -0
  26. package/web/src/App.test.tsx +237 -0
  27. package/web/src/App.tsx +255 -0
  28. package/web/src/api.ts +20 -0
  29. package/web/src/components/Actions.tsx +60 -0
  30. package/web/src/components/Approvals.tsx +62 -0
  31. package/web/src/components/CapabilitySearch.tsx +73 -0
  32. package/web/src/components/Explore.tsx +41 -0
  33. package/web/src/components/Inventory.tsx +124 -0
  34. package/web/src/components/LaunchInstanceModal.test.tsx +97 -0
  35. package/web/src/components/LaunchInstanceModal.tsx +236 -0
  36. package/web/src/components/Library.tsx +76 -0
  37. package/web/src/components/Running.tsx +60 -0
  38. package/web/src/components/Sessions.test.tsx +125 -0
  39. package/web/src/components/Sessions.tsx +189 -0
  40. package/web/src/components/StartSessionModal.test.tsx +86 -0
  41. package/web/src/components/StartSessionModal.tsx +168 -0
  42. package/web/src/components/Welcome.tsx +474 -0
  43. package/web/src/main.tsx +11 -0
  44. package/web/src/styles.css +254 -0
  45. package/web/src/types.ts +47 -0
  46. package/web/src/useDebounce.ts +10 -0
  47. package/web/src/useSession.ts +220 -0
  48. package/web/src/util.test.ts +30 -0
  49. package/web/src/util.ts +17 -0
@@ -0,0 +1,158 @@
1
+ // End-to-end data-path smoke: executor fixture (admin) -> Bridge (/api/inventory) -> served screen.
2
+ // Self-contained (own ports); no deps. Exits non-zero on failure.
3
+ import assert from 'node:assert/strict';
4
+ import { createExecutor } from '../../mock-executor/src/server.mjs';
5
+ import { createBridge } from './server.mjs';
6
+
7
+ const mock = createExecutor();
8
+ await new Promise((r) => mock.listen(0, '127.0.0.1', r));
9
+ const executorUrl = `http://127.0.0.1:${mock.address().port}`;
10
+
11
+ const bridge = createBridge({ executorUrl, allowMockExecutor: true });
12
+ await new Promise((r) => bridge.listen(0, '127.0.0.1', r));
13
+ const base = `http://127.0.0.1:${bridge.address().port}`;
14
+ // authed fetch helper — every /api/ call carries the per-launch bearer token
15
+ const f = (p, o = {}) => fetch(base + p, { ...o, headers: { ...(o.headers || {}), authorization: 'Bearer ' + bridge.cockpitToken } });
16
+
17
+ try {
18
+ // auth gate: /api/ without the token is 401; /healthz is open
19
+ assert.equal((await fetch(`${base}/api/inventory`)).status, 401, 'gate: no token -> 401');
20
+ assert.equal((await fetch(`${base}/api/inventory?token=wrong`)).status, 401, 'gate: bad token -> 401');
21
+ assert.equal((await fetch(`${base}/healthz`)).status, 200, 'healthz open (no token)');
22
+
23
+ // data path: Bridge reads the executor admin inventory
24
+ const r = await f('/api/inventory');
25
+ assert.equal(r.status, 200, 'inventory 200 (authed)');
26
+ const inv = await r.json();
27
+ assert.equal(inv.count, 4, 'four demo instances');
28
+ const ids = inv.instances.map((i) => i.id);
29
+ assert.ok(ids.includes('550e8400-e29b-41d4-a716-446655440000'), 'default instance present');
30
+ assert.equal(inv.instances.find((i) => i.runtime === 'host')?.runtime_posture.isolation, 'least', 'host is least-isolated');
31
+ assert.equal(inv.instances.find((i) => i.runtime === 'wasm-edge')?.runtime_posture.isolation, 'opaque', 'future runtime is opaque');
32
+ assert.equal(inv.instances.find((i) => i.transport?.mode === 'shared-secret')?.transport.trust, 'compatibility', 'legacy secret transport is compatibility posture');
33
+ const i0 = inv.instances[0];
34
+ for (const k of ['id', 'runtime', 'loadout', 'state', 'tenant', 'card_url', 'runtime_posture', 'host_daemon', 'transport', 'launch_context', 'session_backends']) assert.ok(k in i0, `field ${k}`);
35
+ assert.ok(['vm', 'container', 'host', 'wasm-edge'].includes(i0.runtime), 'runtime kind');
36
+
37
+ // running board: seeded working tasks on the running instances
38
+ const rr = await f("/api/running");
39
+ assert.equal(rr.status, 200, 'running 200');
40
+ const run = await rr.json();
41
+ assert.ok(run.count >= 2, 'at least two running tasks seeded');
42
+ for (const k of ['instance_id', 'task_id', 'state', 'tenant']) assert.ok(k in run.running[0], `running field ${k}`);
43
+ for (const k of ['runtime_posture', 'transport']) assert.ok(k in run.running[0], `running posture field ${k}`);
44
+ assert.equal(run.running[0].state, 'working', 'running task is working');
45
+
46
+ // sessions: the demo pty session is listed with a direct ws attach_url
47
+ const sr = await f("/api/sessions?instance=550e8400-e29b-41d4-a716-446655440000");
48
+ assert.equal(sr.status, 200, 'sessions 200');
49
+ const sess = await sr.json();
50
+ const demo = sess.sessions.find((s) => s.id === 'demo-shell');
51
+ assert.ok(demo, 'demo-shell session present');
52
+ assert.match(demo.attach_url, /^ws:\/\/.*\/agents\/.*\/sessions\/demo-shell\/attach$/, 'ws attach_url shape');
53
+ assert.ok(demo.seq >= 3, 'demo session has a seeded transcript');
54
+ assert.equal(demo.mode, 'direct', 'demo session mode');
55
+ assert.equal(demo.backend, 'native', 'demo session backend');
56
+ assert.equal(demo.role_policy, 'observe-default', 'session role policy');
57
+
58
+ // missing instance param is a 400
59
+ assert.equal((await f("/api/sessions")).status, 400, 'sessions requires instance');
60
+
61
+ // loadout catalog passthrough — the start-session picker offers the full set (#1641)
62
+ const lo = await (await f('/api/loadouts')).json();
63
+ assert.ok(Array.isArray(lo.loadouts) && lo.loadouts.length >= 3, 'loadout catalog returned');
64
+ assert.ok(lo.loadouts.every((l) => typeof l.id === 'string' && typeof l.label === 'string'), 'loadouts carry id+label');
65
+ assert.ok(lo.loadouts.some((l) => l.id === 'security-audit'), 'catalog includes a non-default loadout');
66
+
67
+ // registry binding: discover + show through the aiwg CLI (#1592)
68
+ const cap = await (await f("/api/capabilities?q=" + encodeURIComponent("deploy production") + "&limit=4")).json();
69
+ assert.ok(Array.isArray(cap.results) && cap.results.length >= 1, 'discover returns results');
70
+ const hit = cap.results.find((r) => r.name === 'flow-deploy-to-production');
71
+ assert.ok(hit, 'flow-deploy-to-production discoverable');
72
+ assert.ok(hit.name && hit.type, 'result carries name+type for show');
73
+ const shown = await (await f("/api/show?type=skill&name=flow-deploy-to-production")).json();
74
+ assert.match(shown.body, /name:\s*flow-deploy-to-production/, 'show returns the skill body');
75
+ assert.equal((await f("/api/capabilities")).status, 400, 'capabilities requires q');
76
+ // show by discovered PATH — deterministic, sidesteps ambiguous same-named artifacts (#1643)
77
+ const shownByPath = await (await f(`/api/show?path=${encodeURIComponent(hit.path)}`)).json();
78
+ assert.match(shownByPath.body, /name:\s*flow-deploy-to-production/, 'show-by-path returns the body');
79
+ assert.equal(shownByPath.path, hit.path, 'show-by-path echoes the resolved path');
80
+ // a missing artifact is a 4xx, never a 502 (ambiguous/not-found map to operator-correctable input)
81
+ assert.equal((await f('/api/show?type=agent&name=__definitely_not_a_real_artifact__')).status, 404, 'unknown artifact -> 404 not 502');
82
+ // a path outside the AIWG corpus is refused (no traversal)
83
+ assert.equal((await f(`/api/show?path=${encodeURIComponent('/etc/passwd')}`)).status, 400, 'path outside corpus -> 400');
84
+
85
+ // contribution model: actions INJECT a command into a session — the Cockpit never
86
+ // runs the CLI (adr-cockpit-session-control-not-cli-runner) (#1591)
87
+ const contrib = await (await f("/api/contributions")).json();
88
+ assert.ok(contrib.sources.some((s) => s.id === 'aiwg-core'), 'aiwg-core contribution loaded');
89
+ const audit = contrib.actions.find((a) => a.id === 'audit-issues');
90
+ assert.ok(audit && typeof audit.inject.command === 'string', 'audit-issues declares an inject command');
91
+ assert.match(audit.inject.command, /issue-audit/, 'audit-issues injects the issue-audit command');
92
+ // the spawn-aiwg run endpoint is removed
93
+ assert.equal((await f("/api/actions/audit-issues/run", { method: 'POST' })).status, 404, 'action run endpoint gone (no Bridge CLI run for actions)');
94
+
95
+ // management: lifecycle (UC-012)
96
+ const stoppedId = '9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b';
97
+ assert.equal((await (await f(`/api/instances/${stoppedId}/start`, { method: 'POST' })).json()).state, 'running', 'start -> running');
98
+ assert.equal((await (await f(`/api/instances/${stoppedId}/stop`, { method: 'POST' })).json()).state, 'stopped', 'stop -> stopped');
99
+
100
+ // management: cancel a running task
101
+ const before = await (await f('/api/running')).json();
102
+ const victim = before.running[0];
103
+ assert.equal((await f(`/api/tasks/${victim.instance_id}/${victim.task_id}/cancel`, { method: 'POST' })).status, 200, 'task cancel 200');
104
+ const after = await (await f('/api/running')).json();
105
+ assert.ok(after.count < before.count, 'cancel removed a running task');
106
+
107
+ // approval inbox (UC-009)
108
+ const pend = await (await f('/api/approvals?status=pending')).json();
109
+ assert.ok(pend.approvals.length >= 2, 'pending approvals seeded');
110
+ const apr = await (await f('/api/approvals/apr-001?decision=approve', { method: 'POST' })).json();
111
+ assert.equal(apr.status, 'approved', 'approval resolves to approved');
112
+ const pend2 = await (await f('/api/approvals?status=pending')).json();
113
+ assert.equal(pend2.approvals.length, pend.approvals.length - 1, 'approved item leaves the queue');
114
+
115
+ // cost rollup (UC-010)
116
+ const cost = await (await f('/api/cost')).json();
117
+ assert.ok(cost.total.usd > 0 && cost.per_instance.length >= 1, 'cost rollup present');
118
+
119
+ // destroy
120
+ assert.equal((await (await f(`/api/instances/${stoppedId}`, { method: 'DELETE' })).json()).destroyed, stoppedId, 'destroy returns id');
121
+
122
+ // user asset library: clone a catalog asset into the library, list it, delete it.
123
+ // (AIWG source is read-only — clone copies into ~/.aiwg/cockpit/library, never the reverse.)
124
+ const cloneRes = await f(`/api/library/clone?type=${encodeURIComponent(hit.type)}&name=${encodeURIComponent(hit.name)}&path=${encodeURIComponent(hit.path)}`, { method: 'POST' });
125
+ assert.ok([201, 400].includes(cloneRes.status), 'clone returns 201 (new) or 400 (already present)');
126
+ const lib1 = await (await f('/api/library')).json();
127
+ assert.ok(lib1.library.some((a) => a.name === hit.name), 'cloned asset appears in the user library');
128
+ assert.equal((await f(`/api/library/${encodeURIComponent(hit.name)}`, { method: 'DELETE' })).status, 200, 'library delete 200');
129
+ const lib2 = await (await f('/api/library')).json();
130
+ assert.ok(!lib2.library.some((a) => a.name === hit.name), 'deleted asset removed from library');
131
+ // a path that escapes the library is refused
132
+ assert.equal((await f('/api/library/..%2f..%2fevil', { method: 'DELETE' })).status, 404, 'escape attempt refused');
133
+
134
+ // start a session (onboarding primary verb): create + issue a ws attach_url
135
+ const started = await (await f('/api/instances/550e8400-e29b-41d4-a716-446655440000/sessions', { method: 'POST' })).json();
136
+ assert.match(started.id ?? '', /^sess-/, 'start-session returns a new session id');
137
+ assert.match(started.attach_url ?? '', /\/sessions\/sess-[^/]+\/attach$/, 'start-session issues a ws attach_url');
138
+
139
+ // app shell served with the per-launch token injected (React build if present, else
140
+ // the legacy fallback — both carry the title + token)
141
+ const html = await (await fetch(base + "/")).text();
142
+ assert.match(html, /AIWG.?Cockpit/i, 'app title rendered');
143
+ assert.ok(html.includes(`window.__COCKPIT_TOKEN__=${JSON.stringify(bridge.cockpitToken)}`), 'token injected into the served app');
144
+ // strip HTML comments BEFORE matching — a module script trapped inside a comment
145
+ // (the Vite '</head>'-in-comment gotcha) must not count as "referenced".
146
+ const live = html.replace(/<!--[\s\S]*?-->/g, '');
147
+ const shell = /assets\//.test(html) ? 'react' : 'legacy';
148
+ if (shell === 'react') {
149
+ const asset = live.match(/<script[^>]+type="module"[^>]+src="([^"]*assets\/[^"]+\.js)"/);
150
+ assert.ok(asset, 'React build present → module bundle must be referenced outside comments');
151
+ assert.equal((await fetch(base + asset[1].replace(/^\.\//, '/'))).status, 200, 'built React bundle served');
152
+ }
153
+
154
+ console.log(`SMOKE OK — inventory(4) + running(${run.count}) + sessions(demo-shell) + registry(discover→${cap.results.length}) + contrib(${contrib.actions.length}) + shell(${shell})`);
155
+ } finally {
156
+ bridge.close();
157
+ mock.close();
158
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "./contribution.schema.json",
3
+ "id": "aiwg-core",
4
+ "version": "1.0.0",
5
+ "title": "AIWG Core Actions",
6
+ "contributes": {
7
+ "actions": [
8
+ { "id": "audit-issues", "title": "Audit Issues", "icon": "🔍", "group": "issues", "inject": { "command": "/issue-audit", "target": "focused" } },
9
+ { "id": "address-issues", "title": "Address Issues", "icon": "🛠️", "group": "issues", "inject": { "command": "/address-issues", "target": "focused", "needs_args": true, "args_hint": "issue numbers, space-separated" } },
10
+ { "id": "doctor", "title": "Doctor", "icon": "🩺", "group": "maintenance", "inject": { "command": "/aiwg-doctor", "target": "focused" } }
11
+ ],
12
+ "screens": [],
13
+ "hooks": []
14
+ }
15
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://aiwg.io/cockpit/contribution/v1",
4
+ "title": "AIWG Cockpit UI Contribution (#1591)",
5
+ "description": "Declarative extension of the Cockpit UI: screens, actions, and event-hooks. Loaded from apps/cockpit/contrib/ and (future) installed AIWG extensions. An action INJECTS a command into an agentic session (focused, else a new one) — the Cockpit never runs the CLI itself; the agent in the session does. See adr-cockpit-session-control-not-cli-runner.md.",
6
+ "type": "object",
7
+ "required": ["id", "version", "contributes"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$" },
12
+ "version": { "type": "string" },
13
+ "title": { "type": "string" },
14
+ "contributes": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "properties": {
18
+ "actions": {
19
+ "type": "array",
20
+ "items": {
21
+ "type": "object",
22
+ "required": ["id", "title", "inject"],
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$" },
26
+ "title": { "type": "string" },
27
+ "icon": { "type": "string" },
28
+ "group": { "type": "string" },
29
+ "inject": {
30
+ "type": "object",
31
+ "required": ["command"],
32
+ "additionalProperties": false,
33
+ "properties": {
34
+ "command": { "type": "string", "description": "command/prompt injected into an agentic session" },
35
+ "target": { "type": "string", "enum": ["focused", "new"], "default": "focused", "description": "focused = inject into the attached session, else offer a new one; new = always a fresh session" },
36
+ "needs_args": { "type": "boolean", "description": "prompt for arguments before injecting" },
37
+ "args_hint": { "type": "string" }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ },
43
+ "screens": {
44
+ "type": "array",
45
+ "items": {
46
+ "type": "object",
47
+ "required": ["id", "title", "source"],
48
+ "properties": {
49
+ "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$" },
50
+ "title": { "type": "string" },
51
+ "source": { "type": "string", "description": "url or path the screen iframe/region loads" }
52
+ }
53
+ }
54
+ },
55
+ "hooks": {
56
+ "type": "array",
57
+ "items": {
58
+ "type": "object",
59
+ "required": ["on", "action"],
60
+ "properties": {
61
+ "on": { "type": "string", "description": "event name, e.g. session.output, instance.state-changed" },
62
+ "action": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$", "description": "id of a contributed action to run" }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,42 @@
1
+ # AIWG Cockpit — Desktop shell (Tauri v2)
2
+
3
+ A lightweight native window hosting the **same registry-bound Bridge UI** as the
4
+ VS Code shell and the browser. The shell does not replace the CLI or reimplement
5
+ the control plane — `src-tauri/src/main.rs` waits for the Bridge's per-launch
6
+ runtime token file (`~/.aiwg/cockpit/runtime/bridge.json`) and opens a window at
7
+ the Bridge UI with the token on the query string.
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ operator/CLI: aiwg cockpit
13
+ │ (spawns the Bridge; writes runtime/bridge.json mode 600)
14
+ ▼
15
+ Bridge (127.0.0.1:PORT, token-gated /api) ── proxies ──▶ agentic-sandbox executor
16
+ ▲
17
+ │ loads http://127.0.0.1:PORT/?token=…
18
+ desktop window (this app) ◀── same UI ──▶ VS Code webview / browser
19
+ ```
20
+
21
+ ## Build (toolchain-gated)
22
+
23
+ Requires the Rust toolchain + Tauri prerequisites. On Linux: `webkit2gtk-4.1`,
24
+ `libsoup-3.0`, `libappindicator`. Then:
25
+
26
+ ```bash
27
+ cargo install tauri-cli --version '^2' # once
28
+ cd apps/cockpit/desktop
29
+ cargo tauri init # generates icons/ + capabilities/ boilerplate (one-time)
30
+ cargo tauri dev # run against a launched Bridge
31
+ cargo tauri build # produce a bundle
32
+ ```
33
+
34
+ > The repo ships the load-the-Bridge logic (`main.rs`), the Tauri config, and the
35
+ > frontend splash. `cargo tauri init` fills in the platform boilerplate (icons,
36
+ > v2 capabilities) that is environment-specific and not checked in.
37
+
38
+ ## Why a token file (not a socket handshake)
39
+
40
+ The runtime file is the cross-platform handshake every shell shares (see
41
+ `apps/cockpit/shell-core/runtime.mjs`). It is mode `600`; OS-keychain storage is a
42
+ per-platform hardening follow-up (roctinam/aiwg#1595).
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "aiwg-cockpit-desktop"
3
+ version = "0.0.0"
4
+ edition = "2021"
5
+ description = "AIWG Cockpit desktop shell — observe/drive/coordinate multi-stack agentic sessions over the registry-bound Bridge."
6
+ license = "MIT"
7
+
8
+ [build-dependencies]
9
+ tauri-build = { version = "2", features = [] }
10
+
11
+ [dependencies]
12
+ tauri = { version = "2", features = [] }
13
+ serde_json = "1"
14
+
15
+ [[bin]]
16
+ name = "aiwg-cockpit-desktop"
17
+ path = "src/main.rs"
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ tauri_build::build()
3
+ }
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head><meta charset="utf-8" /><title>AIWG Cockpit</title>
4
+ <style>html,body{margin:0;height:100%;background:#0f1115;color:#9aa3b2;font:15px system-ui,sans-serif;display:grid;place-items:center}</style>
5
+ </head>
6
+ <body>
7
+ <!-- Splash only. main.rs opens the real window at the Bridge UI once the runtime
8
+ token file appears; this placeholder satisfies Tauri's frontendDist. -->
9
+ <p>Starting AIWG Cockpit — waiting for the Bridge…</p>
10
+ </body>
11
+ </html>
@@ -0,0 +1,55 @@
1
+ // AIWG Cockpit — Tauri v2 desktop shell (#1594).
2
+ //
3
+ // The desktop window hosts the SAME registry-bound Bridge UI as the VS Code shell
4
+ // and the browser. It does not replace the CLI or reimplement the control plane:
5
+ // it waits for the Bridge's per-launch runtime token file and opens a window at the
6
+ // Bridge UI (token on the query string).
7
+ //
8
+ // Build is toolchain-gated: requires the Rust toolchain + Tauri prerequisites
9
+ // (on Linux, webkit2gtk + libsoup). Run `cargo tauri init` once to generate icons
10
+ // and capabilities, then `cargo tauri build`. See README.md.
11
+ use std::{fs, path::PathBuf, thread, time::Duration};
12
+ use tauri::{WebviewUrl, WebviewWindowBuilder};
13
+
14
+ fn runtime_file() -> PathBuf {
15
+ let home = std::env::var("HOME")
16
+ .or_else(|_| std::env::var("USERPROFILE"))
17
+ .unwrap_or_default();
18
+ PathBuf::from(home).join(".aiwg/cockpit/runtime/bridge.json")
19
+ }
20
+
21
+ /// Read { port, token } from the Bridge runtime file, if present.
22
+ fn read_runtime() -> Option<(u16, String)> {
23
+ let raw = fs::read_to_string(runtime_file()).ok()?;
24
+ let v: serde_json::Value = serde_json::from_str(&raw).ok()?;
25
+ let port = v.get("port")?.as_u64()? as u16;
26
+ let token = v.get("token")?.as_str()?.to_string();
27
+ Some((port, token))
28
+ }
29
+
30
+ fn main() {
31
+ tauri::Builder::default()
32
+ .setup(|app| {
33
+ let handle = app.handle().clone();
34
+ // Poll for the Bridge runtime file (operator/CLI launches `aiwg cockpit`),
35
+ // then open the window at the Bridge UI with the token.
36
+ thread::spawn(move || {
37
+ for _ in 0..100 {
38
+ if let Some((port, token)) = read_runtime() {
39
+ let url = format!("http://127.0.0.1:{port}/?token={token}");
40
+ if let Ok(parsed) = url.parse() {
41
+ let _ = WebviewWindowBuilder::new(&handle, "main", WebviewUrl::External(parsed))
42
+ .title("AIWG Cockpit")
43
+ .inner_size(1100.0, 760.0)
44
+ .build();
45
+ }
46
+ return;
47
+ }
48
+ thread::sleep(Duration::from_millis(150));
49
+ }
50
+ });
51
+ Ok(())
52
+ })
53
+ .run(tauri::generate_context!())
54
+ .expect("error while running AIWG Cockpit");
55
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://schema.tauri.app/config/2",
3
+ "productName": "AIWG Cockpit",
4
+ "version": "0.0.0",
5
+ "identifier": "io.aiwg.cockpit",
6
+ "build": {
7
+ "frontendDist": "../frontend"
8
+ },
9
+ "app": {
10
+ "withGlobalTauri": false,
11
+ "security": {
12
+ "csp": "default-src 'none'; frame-src http://127.0.0.1:* http://localhost:*; style-src 'unsafe-inline'"
13
+ }
14
+ },
15
+ "bundle": {
16
+ "active": true,
17
+ "targets": "all",
18
+ "icon": ["icons/icon.png"]
19
+ }
20
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@aiwg/cockpit",
3
+ "version": "2026.6.3",
4
+ "description": "AIWG Cockpit — UX-first control plane over AIWG + multi-stack agentic sessions. Opt-in, separately published; NOT shipped in the base aiwg npm package (guarded by test/smoke/cockpit-base-footprint.test.js).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "aiwg-cockpit": "bridge/src/server.mjs"
9
+ },
10
+ "files": [
11
+ "README.md",
12
+ "bridge/",
13
+ "contrib/",
14
+ "runtime-docs/",
15
+ "shell-core/",
16
+ "web/dist/",
17
+ "web/index.html",
18
+ "web/src/",
19
+ "web/package.json",
20
+ "vscode/",
21
+ "desktop/README.md",
22
+ "desktop/src-tauri/"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "workspaces": [
28
+ "mock-executor",
29
+ "bridge",
30
+ "vscode",
31
+ "desktop",
32
+ "web"
33
+ ],
34
+ "scripts": {
35
+ "start:bridge": "node bridge/src/server.mjs",
36
+ "dev": "bash scripts/cockpit-dev.sh",
37
+ "build:web": "npm --prefix web install --no-audit --no-fund && npm --prefix web run build",
38
+ "pack:dry": "npm pack --dry-run",
39
+ "publish:dry": "npm publish --dry-run --access public",
40
+ "smoke": "node mock-executor/src/smoke.mjs && node bridge/src/smoke.mjs && node shell-core/smoke.mjs",
41
+ "poc": "node poc/kill-bridge-isolation.mjs && node poc/security-checks.mjs",
42
+ "test:web": "npm --prefix web run typecheck && npm --prefix web run test",
43
+ "test": "npm run smoke && npm run poc",
44
+ "check": "npm run build:web && npm run test:web && npm run test"
45
+ },
46
+ "engines": {
47
+ "node": ">=18"
48
+ }
49
+ }
@@ -0,0 +1,38 @@
1
+ # Cockpit runtime directory — `~/.aiwg/cockpit/runtime/`
2
+
3
+ The Cockpit installs **globally** (one tool at `~/`), and the operator sets the
4
+ working directories that agent instances launch from. Per-launch runtime state for
5
+ the local control surface lives here.
6
+
7
+ ## What lands here
8
+
9
+ | File | Written by | Mode | Contents |
10
+ |---|---|---|---|
11
+ | `bridge.json` | the Bridge on launch | `0600` | `{ token, port, pid, started_at }` — the per-launch handshake every shell reads |
12
+
13
+ The directory itself is `0700`. The Bridge **rewrites** `bridge.json` on each launch
14
+ (the token is per-launch, not persistent).
15
+
16
+ ## How the shells use it
17
+
18
+ Every shell (browser, VS Code, Tauri) resolves the Bridge the same way — see
19
+ `apps/cockpit/shell-core/runtime.mjs`:
20
+
21
+ 1. read `bridge.json` → `{ token, port }`
22
+ 2. wait for `http://127.0.0.1:<port>/healthz`
23
+ 3. load the UI at `http://127.0.0.1:<port>/?token=<token>`
24
+
25
+ ## Security
26
+
27
+ - `bridge.json` holds **only the overlay's own per-launch token** — never a provider
28
+ or stack credential (verified by `apps/cockpit/poc/security-checks.mjs`, property I1).
29
+ - `token` gates every `/api/*` call (constant-time bearer check); `tenant_id` elsewhere
30
+ is a **routing** token, never authentication.
31
+ - OS-keychain storage of the token is the platform-specific hardening follow-up
32
+ (roctinam/aiwg#1595); the `0600` file is the cross-platform handshake.
33
+
34
+ ## Launch-cwd model
35
+
36
+ The Bridge runs on `127.0.0.1`; agent instances launch from operator-set working
37
+ directories (not the install root). Runtime-level operator docs (this directory) are
38
+ distinct from the install (`$AIWG_ROOT`) and from project artifacts (`.aiwg/`).
@@ -0,0 +1,45 @@
1
+ // Shell-core: the handshake every Cockpit shell (VS Code, Tauri, browser) shares.
2
+ // The Bridge writes ~/.aiwg/cockpit/runtime/bridge.json (mode 600) on launch with
3
+ // { token, port }. A shell reads it, waits for liveness, and loads the Bridge UI at
4
+ // <url>/?token=<token>. Control plane is the gated Bridge API; data plane (pty) is
5
+ // the executor URL the Bridge issues. This module is the one source of that contract.
6
+ import { readFile } from 'node:fs/promises';
7
+ import { homedir } from 'node:os';
8
+ import { join } from 'node:path';
9
+
10
+ export const RUNTIME_FILE = join(homedir(), '.aiwg', 'cockpit', 'runtime', 'bridge.json');
11
+
12
+ /** Read the per-launch Bridge connection (token, port, url). Throws if not launched. */
13
+ export async function readRuntime(file = RUNTIME_FILE) {
14
+ const r = JSON.parse(await readFile(file, 'utf8'));
15
+ if (!r.token || !r.port) throw new Error(`runtime file ${file} missing token/port`);
16
+ return { ...r, url: `http://127.0.0.1:${r.port}` };
17
+ }
18
+
19
+ /** Resolve + wait for the Bridge to be reachable; returns { token, port, url }.
20
+ * Polls both the runtime file (it may not exist yet) and liveness. */
21
+ export async function connect({ timeoutMs = 5000, file = RUNTIME_FILE } = {}) {
22
+ const deadline = Date.now() + timeoutMs;
23
+ for (;;) {
24
+ try {
25
+ const rt = await readRuntime(file);
26
+ const live = await fetch(`${rt.url}/healthz`);
27
+ if (live.ok) {
28
+ const authed = await api(rt, '/api/health');
29
+ if (authed.ok) return rt;
30
+ }
31
+ } catch { /* file missing or Bridge not up yet */ }
32
+ if (Date.now() > deadline) throw new Error(`Bridge not reachable (runtime ${file})`);
33
+ await new Promise((r) => setTimeout(r, 100));
34
+ }
35
+ }
36
+
37
+ /** The webview URL a shell loads — Bridge UI with the token on the query string. */
38
+ export function webviewUrl(rt) {
39
+ return `${rt.url}/?token=${encodeURIComponent(rt.token)}`;
40
+ }
41
+
42
+ /** Authed fetch against the Bridge control surface, for shells that call the API directly. */
43
+ export function api(rt, path, opts = {}) {
44
+ return fetch(rt.url + path, { ...opts, headers: { ...(opts.headers || {}), authorization: `Bearer ${rt.token}` } });
45
+ }
@@ -0,0 +1,36 @@
1
+ // Shell handshake smoke: launch the real Bridge CLI (which writes the runtime token
2
+ // file), then drive the shell-core contract both VS Code and Tauri rely on —
3
+ // resolve token+url, confirm liveness, authed call works, unauthed is rejected.
4
+ import assert from 'node:assert/strict';
5
+ import { spawn } from 'node:child_process';
6
+ import { mkdtemp, rm } from 'node:fs/promises';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const BRIDGE = fileURLToPath(new URL('../bridge/src/server.mjs', import.meta.url));
12
+ const PORT = 8147;
13
+ const home = await mkdtemp(join(tmpdir(), 'cockpit-shell-smoke-'));
14
+ process.env.HOME = home;
15
+
16
+ const { connect, webviewUrl, api } = await import('./runtime.mjs');
17
+ const child = spawn(process.execPath, [BRIDGE], {
18
+ env: { ...process.env, HOME: home, PORT: String(PORT), AIWG_COCKPIT_EXECUTOR_URL: 'http://127.0.0.1:1' }, // no live executor needed for the handshake
19
+ stdio: 'ignore',
20
+ });
21
+
22
+ try {
23
+ const rt = await connect({ timeoutMs: 6000 }); // reads ~/.aiwg/cockpit/runtime/bridge.json + waits for /healthz
24
+ assert.equal(rt.port, PORT, 'runtime port matches the launched Bridge');
25
+ assert.ok(rt.token && rt.token.length >= 32, 'runtime carries a per-launch token');
26
+ assert.match(webviewUrl(rt), /\/\?token=/, 'webview url carries the token');
27
+
28
+ // the shell handshake: authed call succeeds, unauthed is gated
29
+ assert.equal((await api(rt, '/api/health')).status, 200, 'authed /api/health 200');
30
+ assert.equal((await fetch(rt.url + '/api/health')).status, 401, 'unauthed /api/health 401');
31
+
32
+ console.log(`SMOKE OK — shell handshake: runtime file -> token+url (:${rt.port}) -> authed Bridge, gate enforced`);
33
+ } finally {
34
+ child.kill();
35
+ await rm(home, { recursive: true, force: true });
36
+ }
@@ -0,0 +1,25 @@
1
+ # AIWG Cockpit — VS Code shell
2
+
3
+ Hosts the registry-bound Cockpit UI inside a VS Code webview and surfaces
4
+ contributed actions as command-palette entries. No build step (CommonJS
5
+ `extension.js`); the same Bridge core as the desktop app and browser.
6
+
7
+ ## Commands
8
+
9
+ | Command | Effect |
10
+ |---|---|
11
+ | **AIWG Cockpit: Open** | Opens the Cockpit UI in a webview (reads the Bridge runtime token, loads `http://127.0.0.1:PORT/?token=…`). |
12
+ | **AIWG Cockpit: Audit Issues** | Runs the contributed `audit-issues` action through the Bridge and prints the result to an output channel. |
13
+
14
+ ## Run it
15
+
16
+ 1. Launch the Bridge: `aiwg cockpit` (or, in-repo, `node apps/cockpit/bridge/src/server.mjs`). It writes `~/.aiwg/cockpit/runtime/bridge.json` (token + port, mode 600).
17
+ 2. In VS Code: **F5** (Extension Development Host) from this folder, or install the packaged `.vsix`.
18
+ 3. Run **AIWG Cockpit: Open** from the command palette.
19
+
20
+ If the Bridge isn't running, the commands show a hint to start it — the shell
21
+ never replaces the CLI; it fronts it.
22
+
23
+ ## Settings
24
+
25
+ - `aiwg-cockpit.bridgeRuntimeFile` — override the runtime file path (default `~/.aiwg/cockpit/runtime/bridge.json`).