Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e3a688f
feat(app-plugins): Lane A build-order 0-2 — unstable feature, app::* …
adbarc92 Jul 16, 2026
dc37806
feat(cockpit): view-plugin runtime — store command-sink, MessagePort …
adbarc92 Jul 17, 2026
998a000
docs(swarm): reconcile plugin-runtime handoff to current main (spike …
adbarc92 Jul 17, 2026
a95a6bf
merge(lane-v): view-plugin runtime core — store command-sink, bridge,…
adbarc92 Jul 17, 2026
bf6340c
merge(lane-a): app-plugin embedding core — Tauri unstable pin, capabi…
adbarc92 Jul 17, 2026
cda3cc1
feat(cockpit): Lane S — unify plugin runtime into the shell
adbarc92 Jul 17, 2026
684faff
docs(spikes): record plugin-runtime Lane S integration — automated ga…
adbarc92 Jul 17, 2026
72621f4
docs(spikes): rescue the P3 app-plugin spike results
adbarc92 Aug 9, 2026
725b630
Merge remote-tracking branch 'origin/main' into feat/plugin-runtime
adbarc92 Aug 11, 2026
db74a47
fix(app-plugins): dispatch plugin_launch off the main thread
adbarc92 Aug 11, 2026
a3edc78
docs(session): wrap 2026-08-10 — smoke run 1, the main-thread fix, wh…
adbarc92 Aug 11, 2026
0d05f55
test(cockpit): cover Gate 5 teardown selection and ratchet the main-t…
adbarc92 Aug 15, 2026
f4d7f38
docs(testing): risk-ranked testing plan, first run
adbarc92 Aug 15, 2026
458b952
Merge remote-tracking branch 'origin/main' into feat/plugin-runtime
adbarc92 Aug 15, 2026
42e290e
style: apply cargo fmt to the code this branch adds
adbarc92 Aug 15, 2026
0466450
docs(status): CI is a real gate now; record the 2026-08-15 integratio…
adbarc92 Aug 15, 2026
14df379
docs(handoff): run Smoke run 2 and unblock PR #49
adbarc92 Aug 15, 2026
26cbd24
docs(status): point #51 at the smoke handoff brief
adbarc92 Aug 15, 2026
55b0a5b
fix(view-plugins): resolve the packaged iframe src to the Windows sch…
adbarc92 Aug 15, 2026
2ab1b49
fix(shutdown): prevent only the first ExitRequested, so the app actua…
adbarc92 Aug 15, 2026
44ee6ad
docs(spike): record Smoke run 2 — db74a47 confirmed, 4 defects found
adbarc92 Aug 15, 2026
e6decdc
docs(status): record Smoke run 2 and re-scope what blocks #49
adbarc92 Aug 15, 2026
7a05cae
docs(handoff): fix D-7, then run the packaged Part 2 smoke for #49
adbarc92 Aug 15, 2026
6b37b20
docs(handoff): grill the operator, then build the smoke-testing skill…
adbarc92 Aug 15, 2026
c16356a
fix(plugins): deliver state to view-plugins and ship the plugin root …
adbarc92 Aug 16, 2026
f275c44
fix(plugins): enforce the manifest's capability grant, failing closed…
adbarc92 Aug 16, 2026
05c95ca
style(tests): rustfmt the D-8 guard, and correct a false claim about CI
adbarc92 Aug 16, 2026
052442a
docs(session): wrap 2026-08-16 — packaged smoke done, #49 ready, reco…
adbarc92 Aug 16, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@
# embargo guard denylist — digests of low-entropy tokens are crackable, so the
# denylist is never committed (see scripts/embargo-guard.mjs)
/.embargo-guard.local.json

# Root-level tooling artifacts (cockpit/ui has its own ignore for its tree).
node_modules/
38 changes: 38 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
# CLAUDE.md

<!-- BEGIN: ACTIVE-SESSION-PICKUP — remove this block when feat/plugin-runtime (PR #49) merges to main -->
## Active session pickup

If the current branch is `feat/plugin-runtime` (check with `git rev-parse --abbrev-ref HEAD`), read
the **State summary** in [`docs/STATUS.md`](docs/STATUS.md) and the **"Smoke run 3"** section of
[`spikes/SPIKE-RESULTS.md`](spikes/SPIKE-RESULTS.md) before doing anything else.

The one-line version: **the smoke is finished. Part 1 and the packaged Part 2 have both been run,
and #49 is READY FOR REVIEW with all 18 CI checks green** — it is waiting on a human merge decision,
nothing else. Run 3 scored **9 PASS / 2 BLOCKED / 2 NOT RUN / 0 FAIL** and closed five defects:
**D-7** (view-plugins got no state — `DataCloneError` posting Svelte `$state` proxies), **D-8** (the
packaged bundle shipped **no plugin root at all**, so no shipped build could load a view-plugin),
**D-2** (every plugin was granted every capability; now fails closed), **D-4** (re-verified packaged:
exit in 0.23 s cold, 5.27 s with 10 containers), and **D-5** (investigated, **did not reproduce**).

Traps, updated:
- **Items 1.2 / 1.4a / 1.7 are BLOCKED, not broken**, behind **D-3**: fleetd serves no CORS headers,
so no browser `fetch` from the cockpit reaches the daemon. **Pre-existing on `main`** — the FLEET
ops grid renders nothing because of this. Don't chase it as a #49 regression; it needs its own issue.
- **Assert Gate 5 with `docker ps -a`, not `docker ps`.** `docker ps` cannot see the `Created` /
`Exited` residue teardown leaves, and that residue breaks the *next* launch with a name conflict.
- **Quit the cockpit gracefully, never `Stop-Process`,** when testing Gate 5 — a force-kill skips
`stop_all_owned` and fabricates a teardown failure.
- **A clean packaged run shows no AUDIENCE tab, and that is correct.** `PluginManager::roots()` has
no packaged resource root by design; app-plugins come from `CC_APP_PLUGINS_DEV` or
`~/.command-center/app-plugins`. Set `CC_APP_PLUGINS_DEV` to drive AUDIENCE, and record it as
"packaged binary, dev discovery seam".
- **A release build has no devtools.** F12 does nothing, so any check whose criterion is a console
reading is NOT RUN when packaged — decide that up front rather than mid-session.
- **"CI never builds the app" is FALSE.** `ci.yml:311` runs `tauri build` on all three OSes. What no
gate does is look *inside* the bundle — which is exactly how D-8 passed a *successful* build. This
is `GAP-132` in [`docs/testing/PLAN.md`](docs/testing/PLAN.md).
- **"Images are prebuilt so there's no build" is false** — `compose build` runs regardless. Though in
run 3 the images already existed and the ramp took ~15 s, not 20 min.

If the branch has changed, this section and its links are stale — delete this block.
<!-- END: ACTIVE-SESSION-PICKUP -->

Project conventions live in the global `~/.claude/CLAUDE.md` and this project's memory store
(`MEMORY.md` index, auto-loaded at session start).

Expand Down
84 changes: 84 additions & 0 deletions cockpit/plugin-sdk/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Type declarations for the cockpit view-plugin SDK (hand-written; the runtime is
// plain browser ESM in index.js). Mirrors the host bridge protocol.

export type PluginTier = 't1' | 't2' | 't3';
export type PluginMode = 'demo' | 'real';
export type PluginUnitAction = 'halt' | 'resume' | 'abandon' | 'ship';

export interface LogDelta {
seq: number;
stream: string;
line: string;
}

/** A unit projection minus its heavy log (history capped by the host). */
export interface UnitLite {
id: string;
task: string;
tier: string;
phase: string;
history: string[];
cost: number;
usdCap: number;
tokensIn: number;
tokensOut: number;
iters: { build: number; check: number; review: number };
findings: { round: number; title: string; severity: string; resolved: boolean }[];
oracleFiles: string[];
branch?: string;
pr?: string;
blocked?: string;
awaitingSlot: boolean;
rateLimited: boolean;
error?: string;
result?: string;
lastSeq: number;
}

export interface StateMessage {
v: 1;
type: 'state';
full: boolean;
changed: UnitLite[];
removed: string[];
order: string[];
degraded: boolean;
}

export interface CommandAck {
v: 1;
type: 'command-ack';
reqId: string;
ok: boolean;
reasonClass?: string;
}

export interface LaunchReq {
task: string;
tier: PluginTier;
mode: PluginMode;
min_review_rounds?: number;
}

export interface PluginClient {
apiVersion?: number;
capabilities: string[];
onState(cb: (s: StateMessage) => void): () => void;
onLog(cb: (unitId: string, lines: LogDelta[]) => void): () => void;
onReset(cb: () => void): () => void;
onAck(cb: (ack: CommandAck) => void): () => void;
launch(req: LaunchReq): Promise<CommandAck>;
command(unitId: string, action: PluginUnitAction): Promise<CommandAck>;
}

export interface ConnectOpts {
scope?: unknown;
parent?: unknown;
timeoutMs?: number;
}

export function connect(opts?: ConnectOpts): Promise<PluginClient>;
export function attach(
port: MessagePort,
init?: { apiVersion?: number; capabilities?: string[] },
): PluginClient;
155 changes: 155 additions & 0 deletions cockpit/plugin-sdk/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// @ts-nocheck
// Cockpit view-plugin SDK (Lane V) — bundled convenience for UNTRUSTED plugins that run
// in a sandboxed iframe. It speaks the MessagePort protocol the host bridge expects:
//
// connect() → posts `plugin-hello` to the parent, awaits the host's `init` (which
// transfers a private MessagePort), replies `ready`, then exposes typed callbacks and
// promise-returning command verbs. All traffic after the handshake is on the port.
//
// The plugin never sees the daemon URL, host DOM, storage, or network — only this port.
// `connect()` accepts an injectable `scope`/`parent` purely so it is testable off a real
// window; in the iframe it defaults to `window` / `window.parent`.

const PROTOCOL_VERSION = 1;

/**
* Perform the plugin-announces-ready handshake and resolve to a connected client.
* @param {{ scope?: any, parent?: any, timeoutMs?: number }} [opts]
* @returns {Promise<PluginClient>}
*/
export function connect(opts = {}) {
const scope = opts.scope ?? (typeof window !== 'undefined' ? window : undefined);
const parent = opts.parent ?? (scope ? scope.parent : undefined);
if (!scope || !parent) {
return Promise.reject(new Error('cockpit-sdk: no window/parent to connect through'));
}
return new Promise((resolve, reject) => {
let settled = false;
let timer = null;

function onMessage(e) {
const d = e && e.data;
if (!d || d.v !== PROTOCOL_VERSION || d.type !== 'init') return;
const port = e.ports && e.ports[0];
if (!port) return;
settled = true;
if (timer !== null) clearTimeout(timer);
scope.removeEventListener('message', onMessage);
resolve(attach(port, d));
}

scope.addEventListener('message', onMessage);
// The plugin announces readiness FIRST (avoids a host `load`-post race). `"*"` is safe:
// the hello is non-sensitive and the sandbox frame's real origin is the unusable "null".
parent.postMessage({ v: PROTOCOL_VERSION, type: 'plugin-hello' }, '*');

if (opts.timeoutMs) {
timer = setTimeout(() => {
if (settled) return;
scope.removeEventListener('message', onMessage);
reject(new Error('cockpit-sdk: handshake timed out'));
}, opts.timeoutMs);
}
});
}

/**
* Wrap an already-transferred MessagePort as a client. Exposed for tests that drive the
* port directly (the normal path is `connect()`).
* @param {MessagePort} port
* @param {{ apiVersion?: number, capabilities?: string[] }} init
* @returns {PluginClient}
*/
export function attach(port, init = {}) {
const stateCbs = [];
const logCbs = [];
const resetCbs = [];
const ackCbs = [];
const pending = new Map(); // reqId → resolve
let reqSeq = 0;

port.onmessage = (e) => {
const m = e && e.data;
if (!m || m.v !== PROTOCOL_VERSION) return;
switch (m.type) {
case 'state':
for (const cb of stateCbs) cb(m);
break;
case 'log-append':
for (const cb of logCbs) cb(m.unitId, m.lines);
break;
case 'log-reset':
for (const cb of resetCbs) cb();
break;
case 'command-ack': {
for (const cb of ackCbs) cb(m);
const resolve = pending.get(m.reqId);
if (resolve) {
pending.delete(m.reqId);
resolve(m);
}
break;
}
default:
// forward-compatible: ignore unknown host messages
break;
}
};
if (port.start) port.start();

// Reply `ready` — the host answers with a full `state` snapshot.
port.postMessage({ v: PROTOCOL_VERSION, type: 'ready' });

function send(payload) {
const reqId = `p${reqSeq++}`;
return new Promise((resolve) => {
pending.set(reqId, resolve);
port.postMessage({ v: PROTOCOL_VERSION, type: 'command', reqId, ...payload });
});
}

return {
apiVersion: init.apiVersion,
capabilities: init.capabilities ?? [],
/** Dirty-delta (or full) state pushes: `{ full, changed, removed, order, degraded }`. */
onState(cb) {
stateCbs.push(cb);
return () => {
const i = stateCbs.indexOf(cb);
if (i >= 0) stateCbs.splice(i, 1);
};
},
/** Append-only log deltas: `(unitId, lines)` where each line is `{ seq, stream, line }`. */
onLog(cb) {
logCbs.push(cb);
return () => {
const i = logCbs.indexOf(cb);
if (i >= 0) logCbs.splice(i, 1);
};
},
/** Fired on a daemon-stream reconnect: discard per-unit log cursors (a full state follows). */
onReset(cb) {
resetCbs.push(cb);
return () => {
const i = resetCbs.indexOf(cb);
if (i >= 0) resetCbs.splice(i, 1);
};
},
/** Every `command-ack`, correlated by `reqId` (also resolves the originating promise). */
onAck(cb) {
ackCbs.push(cb);
return () => {
const i = ackCbs.indexOf(cb);
if (i >= 0) ackCbs.splice(i, 1);
};
},
/** Request a launch. Resolves to the `command-ack` (ok, or rejected with reasonClass). */
launch(req) {
return send({ launch: req });
},
/** Request a unit command (halt/resume/abandon/ship). Resolves to its `command-ack`. */
command(unitId, action) {
return send({ unit: { id: unitId, action } });
},
};
}
16 changes: 16 additions & 0 deletions cockpit/plugin-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@cockpit/plugin-sdk",
"version": "0.1.0",
"private": true,
"description": "Bundled convenience SDK for cockpit view plugins (sandboxed-iframe MessagePort client).",
"type": "module",
"main": "index.js",
"module": "index.js",
"types": "index.d.ts",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
}
}
7 changes: 6 additions & 1 deletion cockpit/ui/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
thiserror = "2"
log = "0.4"
tauri = { version = "2.11.2", features = [] }
# LANE-A (app-plugins): the child-webview embedding layer needs Tauri's `unstable`
# feature (multiwebview: create/position/show/hide child webviews). `unstable` is
# explicitly NOT semver-stable, so the version is PINNED with `=` — a patch bump can
# silently change or remove the unstable webview APIs. Note for CI/upgrade docs: bumping
# this line requires re-verifying the embedding smoke (spec §6 step 0), not a blind bump.
tauri = { version = "=2.11.2", features = ["unstable"] }
tauri-plugin-log = "2"
tauri-plugin-shell = "2"
# LANE-P (packaging): activate the updater runtime so the `plugins.updater`
Expand Down
42 changes: 42 additions & 0 deletions cockpit/ui/src-tauri/app-plugins/audience/app-plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"id": "audience",
"name": "Audience",
"apiVersion": 1,
"icon": "icon.svg",
"url": "http://localhost:3000",

"lifecycle": {
"managed": true,
"cwd": "D:/MajorProjects/CURRENT/audience",

"build": {
"cmd": "docker compose -f docker-compose.prod.yml build",
"args": {
"NODE_ENV": "development",
"AI_PROVIDER": "fake",
"MEDIA_PROVIDER": "fake"
},
"timeout": 1200000
},

"start": "docker compose -f docker-compose.prod.yml up",
"stop": "docker compose -f docker-compose.prod.yml down",

"env": {
"NODE_ENV": "development",
"AI_PROVIDER": "fake",
"MEDIA_PROVIDER": "fake",
"DEV_WORKSPACE_ID": "ws_dev_cockpit",
"DEV_USER_ID": "user_dev_cockpit"
},

"health": { "url": "http://localhost:8080/health", "okStatus": [200], "timeout": 180000, "interval": 1000 },
"ready": { "url": "http://localhost:3000", "okStatus": [200, 204, 301, 302, 307, 308], "timeout": 180000, "interval": 1000 }
},

"webview": {
"popups": "allow",
"externalLinks": "in-app",
"title": "Audience"
}
}
Loading
Loading