Cockpit plugin runtime — view-plugins + app-plugins unified into the shell - #49
Conversation
…caps, Audience manifest - Cargo.toml: enable Tauri `unstable` feature (child-webview multiwebview APIs) and PIN with `=2.11.2` (unstable is not semver-stable; note for upgrade docs). - capabilities/default.json: add isolated `app-plugins` capability scoped to the `app::*` webview-label glob (spec §6 step 0) granting host visibility/position/focus webview commands only — no core:default/shell (spec §5 no host<->app bridge). - Audience proving manifest (app-plugins/audience/app-plugin.json): credential-free dev path — fake providers as BUILD args (baked), devAuth via runtime env + DEV_WORKSPACE_ID/ DEV_USER_ID; ready probe accepts 3xx for the /dashboard redirect. - manager.rs: CC_APP_PLUGINS_DEV dev-list discovery root (user dir still wins on collision) + unit tests for the shipped manifest posture and dev-root discovery. cargo test: 28 passed / 0 failed. Manifest/discovery/lifecycle scaffolds already present on main extended in place, not rewritten.
…bridge+policy, SDK, loader, reference plugin Lane V (Spec-A steps 2–5). Sandboxed-iframe view-plugin runtime: - store.svelte.ts: single command-sink support without breaking the live reconnect/socket path. Adds a plain dirty-set accumulator (drainDirty) the bridge drains on its tick, a seq-tagged per-unit log tail (logsSince) for log-append, and guarded socket-opening (ensureStream) + optimistic-insert guard so a bridge launch racing reconnect() yields exactly one unit + one socket. Preserves the started/seq-dedup/if-exists double-connect guards. - bridge.ts: MessagePort bridge. plugin-hello→init→ready handshake (identity bound to the transferred port, not the null origin); dirty-delta `state` pushes with per-unit lastEmitted suppression + full-snapshot baseline reset; log-append/log-reset; command policy (shape/authority/unknown-id/over-bound), token-bucket rate limit + inbound-flood→port.close() kill; command-ack with reqId correlation; real-launch staged to the host overlay (demo-only for plugins). Coarse `degraded` only — no raw health leaked. - loader.ts: manifest validation + apiVersion gate + capability negotiation + dev|packaged iframe-src resolution behind a single injected PluginSource. - cockpit/plugin-sdk: connect()/attach() plugin client (browser ESM + .d.ts). - plugins/reference: reference plugin exercising the FULL surface (dirty state, log-append, policed demo launch, command-ack rejection, awaiting-approval presence indicator). Pure model.js is unit-tested. Tests: 53 green across store(sink/fold/overlay), bridge (handshake 100×, policy, flood-kill, ack), loader, SDK↔session, reference model. The overlay is unchanged (already built); the bridge reads host-owned awaitingApproval state, it does not drive its own modal.
…provenance + Lane V modify-not-create)
… policy, plugin-sdk, loader, reference plugin
…lities, Audience manifest, dev-list discovery
- tauri.conf.json: host CSP (frame-src http://ccplugin.localhost + fleetd connect-src) - lib.rs: register ccplugin:// scheme (ACAO + plugin-doc CSP header) + plugin_show/hide/set_rect + WebviewPool - view_plugins.rs (new): production ccplugin:// asset handler (dev roots + packaged resources), P4 findings - embedding.rs (new): async app-plugin child-webview show/hide(park)/set_rect + warm-pool LRU, P3 findings - App.svelte: one topbar switcher across Fleet + Projects + view-plugin iframe + app-plugin webview; PluginBridge handshake wiring, hide-on-overlay compositing, ResizeObserver rect tracking Verified: cargo test 28/0, npm test 133/0, npm run check 0 errors, npm run build clean. Ops-grid + overlay regression tests unchanged.
…tes green, manual smoke checklist
This file was referenced by docs/SWARM-HANDOFF-plugin-runtime.md but existed only as an untracked file inside the spike/app-plugins-webview-v2 worktree -- in no commit, on no branch, one 'git clean' from being lost. It is the provenance for this branch's design decisions: the park-off-screen keep-warm choice (hide()/show() forces a repaint/reload, losing scroll and session state), the async-command fix for the add_child deadlock, and the verbatim Tauri 2.11.2 unstable webview API that embedding.rs copies. Records the P3 decision as LEANING GO with packaged gates 2/4 and Gate 5 (lifecycle, no orphans) still open -- both of which remain outstanding and belong to this PR's interactive smoke gate.
The Phase-6 interactive smoke (checklist 1.5) froze the whole UI on the first
AUDIENCE tab activation. `plugin_launch` was a synchronous #[tauri::command], so
it ran on the main event-loop thread -- the same finding that already forced the
embedding commands to be async -- and blocked there on `docker compose build`
plus the health/ready probe budgets. The code carried a standing note predicting
this ("may block up to the probe timeout (~180 s) ... can move to a background
task"); the smoke is what came due.
Dispatch the start sequence to a dedicated OS thread and return immediately. A
plain thread, not an async-runtime worker: every seam in the sequence is
blocking (Command::status, ureq, thread::sleep), so handing it to the runtime
would starve a worker and merely relocate the stall.
That changes the command's contract -- Ok now means "dispatched", not "healthy"
-- so the shell must stop treating it as readiness. App.svelte no longer
fabricates pluginState[id]='healthy' and no longer calls plugin_show directly;
the existing compositing $effect composites on the plugin://state `healthy`
event instead. This half is load-bearing rather than cosmetic: with launch
returning early, the old code would have pointed the child webview at a URL that
is not serving yet. The tab now shows live building -> starting -> healthy
progress instead of a frozen window.
Pinned by src/App.appPlugin.test.ts (verified red -> green): no compositing
before `healthy`, and none at all on `error`.
Gates: cargo test 28 passed; npm test 135 passed (19 files); npm run check 353
files 0 errors/0 warnings; cargo clippy exit 0.
Smoke results recorded in spikes/SPIKE-RESULTS.md. Gate 5 container teardown
PASSES (docker ps empty after quit, 0-container baseline), but the app process
survived the window close -- recorded as an open anomaly, not diagnosed.
…at is left The interactive smoke for PR #49 ran for the first time and failed at item 1.5: plugin_launch blocked the main event-loop thread for the whole docker build. Fixed in db74a47. The fix is verified by cargo test / npm test / npm run check / clippy and a new regression test, but NOT in a watched window -- re-running 1.5 and finishing the other nine dev items plus the packaged pass is the next action. Rewrites the STATUS.md state summary in place, appends the session-log entry, and adds a bounded pickup block to CLAUDE.md pointing at both. Records two traps worth not rediscovering: a running dev app blocks rebuilds of the tauri crate (sidecar file lock), and Gate 5 requires a graceful quit rather than a kill.
Four of the allows I added claimed the code was waiting on the Phase-6 embedding layer. Verified by grep across origin/main and origin/feat/plugin-runtime — that is false for these: state::check_crash only its own two unit tests call it Manifest::webview read only by test assertions WebviewCfg (all fields) read only by test assertions Manifest::window_title one test assertion, no production caller Spawner::has_exited reachable only via check_crash, so dead with it These are unimplemented features with no caller anywhere, not code whose caller is en route. A plugin author setting 'popups: block' today gets no blocking. The allows stay — deleting the code would amputate work in flight on #49 — but the comments now say plainly that nothing calls this on any branch, so the allow cannot quietly become the permanent resting place of two unbuilt features. PluginManager::url_for keeps its original comment: that one is genuinely waiting on a caller, verified at embedding.rs:99 on feat/plugin-runtime. Comments-only change, no code touched.
* ci: gate on rustfmt, clippy, svelte-check and the cockpit crate's tests Four gates the workflow was missing: - cargo fmt --all -- --check - cargo clippy --workspace --all-targets -- -D warnings - npm run check (svelte-check + tsc) - cargo test for cockpit/ui/src-tauri The last one closes a real hole. cockpit/ui/src-tauri declares its own empty [workspace] table, so it is a standalone workspace that the root manifest does not list as a member — 'cargo test --workspace' run from the repo root never reached its tests, and that suite (the app-plugin state machine, manifest validation, discovery) had only ever been run by hand. The fmt and clippy gates run once per manifest for the same reason. Compiling the tauri crate needs the WebKitGTK system deps the build job already installs, plus the fleetd sidecar binary, because tauri-build resolves the externalBin resource at compile time and hard-errors without it. Both the lint and cockpit-test jobs therefore repeat that setup. * style: apply cargo fmt to both cargo workspaces Pure rustfmt output, no behavior change — the new 'cargo fmt --all -- --check' gate's baseline. Neither workspace had ever been run through rustfmt, so this is the whole backlog in one pass; there is no rustfmt.toml, so these are the stock defaults. Mostly struct literals and multi-argument calls being split across lines. * fix(cockpit): clear clippy debt in the src-tauri crate The root workspace was already clean under '-D warnings'; the cockpit crate had never been linted and had twelve findings. Three were real and are fixed: a nonminimal_bool in the discovery filter (the pruned test is now a named binding rather than a negated conjunction) and two unnecessary_to_owned in the check_crash tests. The other nine are dead_code on plugin-runtime scaffolding that is deliberately ahead of its caller — the Phase-6 embedding layer that will read the webview manifest surface, the crash watcher that will poll Spawner::has_exited via check_crash, and the STOPPED rest state. Deleting any of it would amputate an in-flight feature, so each site gets a narrowly scoped #[allow(dead_code)] with a comment naming the caller it is waiting on. No blanket allows, and nothing at module or crate level. * test(cockpit): give each local_projects test its own temp dir The tmp() helper keyed its directory on the process id alone, so all four local_projects tests shared one path while running concurrently in the same test binary. One test's remove_dir_all raced another's create_dir_all and the run failed with AlreadyExists or PermissionDenied — 5 times in 20 on Windows. Pre-existing, and invisible until now because these tests were never run by CI. The new 'cargo test (cockpit)' gate would have inherited it as a ~25% flake rate, so: an atomic counter makes each directory unique. 0 failures in 30 runs after. Test-only change, no production code touched. * test(cockpit): make the absolute-cwd test pass off Windows keeps_absolute_cwd_as_is leaned on the AUDIENCE_JSON fixture's 'D:/…' cwd. That is only an absolute path on Windows; everywhere else Path::is_absolute says no, resolved_cwd joins it to the manifest dir, and the assertion fails. Production code is fine — is_absolute is doing exactly its job. Only the test baked in a platform. It now picks an absolute path the host agrees with. Surfaced by the new 'cargo test (cockpit)' gate, which is the first time this suite has ever run on Linux. * ci: gate on the frontend vitest suite Fifth gate: 'npm test' in cockpit/ui, 91 tests across 13 files. Same shape as the svelte-check job — node only, no Rust, no system deps. Closes a half-gated pair rather than just a coverage gap. The regression pin for the plugin_launch main-thread freeze is split across two suites: a Rust test, now covered by 'cargo test (cockpit)', and cockpit/ui/src/App.appPlugin.test.ts, covered here. That file is not on main yet — it lands with the plugin-runtime work — so this job gates 91 tests today and picks up the JS half of the pin the moment it merges. Wiring it now means the pin is never half-enforced. * docs(cockpit): correct four dead_code allow comments that overstated Four of the allows I added claimed the code was waiting on the Phase-6 embedding layer. Verified by grep across origin/main and origin/feat/plugin-runtime — that is false for these: state::check_crash only its own two unit tests call it Manifest::webview read only by test assertions WebviewCfg (all fields) read only by test assertions Manifest::window_title one test assertion, no production caller Spawner::has_exited reachable only via check_crash, so dead with it These are unimplemented features with no caller anywhere, not code whose caller is en route. A plugin author setting 'popups: block' today gets no blocking. The allows stay — deleting the code would amputate work in flight on #49 — but the comments now say plainly that nothing calls this on any branch, so the allow cannot quietly become the permanent resting place of two unbuilt features. PluginManager::url_for keeps its original comment: that one is genuinely waiting on a caller, verified at embedding.rs:99 on feat/plugin-runtime. Comments-only change, no code touched.
…hread rule
Gate 5 ("quit the app, confirm `docker ps` is clean") was the one merge blocker
on the plugin runtime with no automated coverage at all. It splits in two: WHICH
stacks a teardown pass picks, and WHETHER `docker compose down` brings them down.
Only the first half is testable without a Docker daemon, so extract it.
`teardown_targets` pulls the selection half out of `stop_all_owned` and pins it
with six tests: owned-only (adopted stacks survive a quit), a vanished discovery
record is skipped rather than panicked on inside the shutdown handler, every
owned stack is selected rather than the first, the manifest's own cwd is carried
through, the running map is emptied, and a second pass is a no-op. That last one
bears on the open Gate-5 process-exit anomaly: it rules out a teardown pass
re-spending the 30 s ExitRequested budget.
`tests/tauri_command_threading.rs` makes the main-thread-blocking defect
mechanical. This project has hit it twice -- the embedding commands in P3, then
`plugin_launch` in smoke item 1.5 -- and both times a human watching a window
caught what every automated gate had passed. The test scans for
`#[tauri::command]` and requires each to be `async` or dispatching; the four
pre-existing sync commands are listed as debt with a note on each, three of them
marked UNBOUNDED because they genuinely are. A companion test fails if a debt
entry goes stale, so the ratchet only tightens. It also pins the db74a47 fix from
the Rust side: delete the thread::spawn and this goes red.
cargo test (cockpit): 34 passed, 2 passed. Execution half stays a human gate.
Bootstrap run of the testing-plan skill against a3edc78. Scores 131 gaps on likelihood x impact across seven test tiers, records a trust verdict per runner, and ranks what to close next. The load-bearing discovery is that five of the seven automated tiers did not run in CI: 135 vitest tests, 28 Tauri-host Rust tests, 52 session-state tests and 53 pytest tests were advisory, not gating. `cargo test --workspace` never reached the cockpit crate at all, because `cockpit/ui/src-tauri/Cargo.toml` opens with a bare `[workspace]` and the root manifest lists only crates/fleet-core and crates/fleetd. GAP-057, GAP-111 and GAP-113 are the entries for that; PR #60 closes all three, so those rows and the tier map's `in_ci` flags are already stale and will re-derive on the next run. The app-plugin runtime is a deliberate scope carve-out (section 2): targeted tests for the plugin_launch freeze and the Gate-5 teardown lifecycle were being written concurrently, so GAP-005/009/010 are recorded as context rather than as gaps to act on. The preceding commit is that work; R5 releases them back into the ranking. Six items await human ratification (section 3), including the `spine_weight` table -- which is the Impact axis of every score in the file and is currently a first-run proposal, not a ratified input.
#60 added `cargo fmt -- --check` to CI and applied the formatter to both workspaces on main. This branch predates that gate, so its own additions -- the Gate-5 tests from 0d05f55, the audience-manifest assertions, and the LRU touch in embedding.rs -- were never formatted and would fail the new check job. Whitespace only, stock defaults, no rustfmt.toml. Verified after: fmt --check clean on both manifests, clippy --all-targets -D warnings clean, cargo test 34+2 (cockpit) and 120 (workspace), npm run check 353 files 0/0, vitest 135 in 19 files.
…n pass Rewrites the State summary in place and appends a session-log entry. The audit that opened this session found STATUS.md two sessions behind: it was silent on the 8/13 testing plan, the Gate-5 tests, and PR #60 entirely. Also records two wrong predictions so they are not re-derived -- the #60/#49 merge-tree conflict forecast that did not materialise, and the `gh pr merge --delete-branch` failure that was only a local-branch delete failing after a successful merge.
Self-contained brief for the interactive smoke. Carries the dev-seam env vars (npm run desktop does not set them), the 11-item dev checklist plus the packaged pass, the Gate-5 baseline, and the three traps that have already cost time twice. Records the 1.9b process-exit anomaly as an open question rather than a known issue: it has been carried forward three times undiagnosed, and 0d05f55 has now eliminated one of its hypotheses.
…eme origin `pluginSrc` emitted the literal `ccplugin://localhost/<id>/<entry>`. On Windows/WebView2 — the primary target — a custom scheme is reachable only as `http://<scheme>.localhost/...`; the literal form is an EXTERNAL protocol, and the plugin iframe's `sandbox="allow-scripts"` forbids navigating to one. The frame never navigated and rendered blank: Navigation to external protocol blocked by sandbox, because it doesn't contain any of: 'allow-top-navigation-to-custom-protocols', ... So view-plugins could not load on Windows at all. The codebase already assumed the fixed form in two places the code never produced: the host CSP (`frame-src http://ccplugin.localhost`) and `pluginSrc`'s own docstring. Every automated gate missed this because `loader.test.ts` asserted the broken string as correct, and `tauri build` only compiles — nothing in CI navigates the iframe. Found by the PR #49 interactive smoke (Smoke run 2, item 1.3). `pluginSrc` now takes an injected `isWindows` (defaulting to a UA check, matching Tauri's own convertFileSrc logic) so both branches are unit-testable. Verified live: REFERENCE renders and completes its `plugin-hello`->`ready` handshake, on hot reload and on a cold start.
…lly exits The `ExitRequested` handler called `api.prevent_exit()` unconditionally, ran teardown, then `app_handle.exit(0)` — which re-emits `ExitRequested`. The handler prevented that too, and called itself forever. The process never exited: after a graceful window close it sat with no window, 20 threads, `Responding=True`, spinning at 93.9% of one core, having burned 309s of CPU before it was killed. This is Gate 5 item 1.9b, carried forward undiagnosed since Smoke run 1. It is a real shutdown defect, not a `tauri dev` supervision artifact: `cargo` was blocked *on* the app rather than holding it open, and a hot spin loop is not what supervision looks like. `stop_all_owned_is_idempotent` (0d05f55) made this worse while appearing to rule it out — removing the 30s cost per iteration turned a slow loop into a hot one. It also explains the standing "do not rebuild the tauri crate while the app is running" trap: `tauri-build` cannot overwrite `fleetd-serve.exe` precisely because the app never exits. A `ShutdownGuard` now prevents the first exit request only; every later one passes through. Pinned by `shutdown_guard_tests::only_the_first_exit_request_is_prevented` (verified red->green). Verified in a watched window: the process now exits within ~1s of teardown, total CPU 0.19s (was 309s and climbing), with cargo returning, the sidecar reaped and 8787 released. Known remaining, deliberately not bundled here: `stop_all_owned(30_000)` still runs synchronously inside the RunEvent callback, i.e. on the main event-loop thread, blocking exit for as long as teardown takes (~2.5 min with 10 containers up).
Nine of eleven checklist items had never been executed. Records the full result table plus six findings (D-1..D-6), including the two fixed this session. Headline: db74a47 is CONFIRMED. Responsiveness was measured rather than judged — 1,127 samples at 1Hz with zero unresponsive, spanning a compose build, a failed up, and a clean 0->3->10 container ramp. Also corrects two premises the handoff was written on: the prebuilt images do NOT skip the compose build, and Gate 5's "docker ps empty" criterion uses the wrong instrument — it cannot see the Created/Exited residue that run 1 left behind, which is what broke run 2's first launch.
State summary rewritten in place, session-log entry appended, and the CLAUDE.md pickup block refreshed (it still pointed at Smoke run 1 and claimed 2/11 items done). The picture changed: db74a47 is CONFIRMED by measurement, but #49 is no longer blocked merely on "finish the checklist" — it is blocked on D-7 (view-plugins receive no state), D-2 (capability negotiation is inert), and the packaged Part 2 which has never been run. Also corrects two premises the previous handoff was written on, and records that 1.2/1.4a/1.7 are BLOCKED behind a pre-existing fleetd CORS gap rather than failing.
… with a real RED phase
Smoke run 2 — Part 1 complete.
|
| Commit | Defect |
|---|---|
55b0a5b |
View-plugins could not load on Windows at all. pluginSrc emitted ccplugin://localhost/…; on WebView2 that is an external protocol, and sandbox="allow-scripts" forbids navigating to one, so the frame stayed blank. The host CSP (frame-src http://ccplugin.localhost) and pluginSrc's own docstring already assumed the correct form — the code contradicted both. Verified cold-start. |
2ab1b49 |
The app never exited. ExitRequested called prevent_exit() then exit(0), which re-emits ExitRequested → prevented again → infinite loop. Measured at 93.9% of a core, 309 s of CPU burned, no window. Fixed with a ShutdownGuard; verified in a watched window (exits ~1 s, 0.19 s CPU). |
Both are pinned by tests written red→green.
Why CI never caught either
loader.test.ts:52 asserted the broken URL string as correct, and tauri build only compiles — nothing in the pipeline ever navigates the iframe or closes the window.
More broadly, three of the four defects share one shape: a unit test passes on a function whose output is never wired to anything. negotiateCapabilities is tested while its result is discarded; bridge.test.ts drives fakes, so the real Svelte 5 $state proxies are never exercised. The coverage gap is at integration boundaries, not units.
Staying draft — still blocked on
- D-7 — view-plugins receive no state at all:
sendFullStatethrowsDataCloneErrorposting$stateproxies throughpostMessage, and it throws insideonReadybefore the tick timer starts. Agreed as the next fix. - D-2 — capability negotiation is inert: every plugin is granted every host capability regardless of its manifest. Security-relevant.
- Part 2, the packaged smoke — still never executed.
Not this PR's problem
Items 1.2 / 1.4a / 1.7 are BLOCKED, not failed, behind a pre-existing main defect: fleetd serves no CORS headers (OPTIONS /missions → 405, no ACAO on any response), so no browser fetch from the cockpit reaches the daemon. git diff origin/main...HEAD is empty for api.ts and crates/fleetd/. This is also why the FLEET ops grid renders nothing today.
Also corrected two premises earlier docs were written on: prebuilt images do not skip compose build, and Gate 5's docker ps criterion cannot see the Created residue that broke this run's first launch — it needs docker ps -a.
Follow-up filed as #61 (loading model: blank tabs with no loading or failure affordance).
…when packaged Two defects found by the packaged Part 2 smoke, both fixed test-first. D-7 - view-plugins received no state at all. `makeFleetHost` hands the session live Svelte 5 `$state` proxies (`store.units[id]`, `store.order`), which are not structured-cloneable, so `port.postMessage` threw DataCloneError. The throw landed inside `onReady` before the tick timer was installed, so the plugin got no state AND no ticks - the channel was dead, not stale. Fixed at the single choke point in `PluginSession.post` so `state`, `log-append`, `order` and `degraded` are all covered. `bridge.test.ts` could never have caught this: it drives a hand-rolled host whose units are plain objects. The new `bridge.proxy.svelte.test.ts` uses the real `FleetStore`, real proxies and a real `MessagePort`, and reproduces the DataCloneError exactly. Verified red -> green. D-8 - the view-plugin runtime could not work in ANY packaged build. `plugin_roots` falls back to `resource_dir()/plugins` and `sdk_bytes` to `resource_dir()/plugin-sdk/index.js`, but `tauri.conf.json` had no `bundle.resources` key at all - the bundle shipped only `icon.ico`. Every packaged build would 404 the plugin document and never load the SDK, so every handshake timed out. Found by preflight inspection before the operator touched the keyboard. Pinned by `tests/packaged_plugin_root.rs`, a ratchet rather than a note: delete either resource mapping and it goes red. Both are the same shape as D-1 and D-2 - exercised on the dev path, never wired or asserted on the path that ships. Nothing in CI bundles the app or navigates the iframe, which is why all four passed every gate. Verified in the packaged build: REFERENCE renders (a first), handshake completes, the state badge leaves its static default, 632 responsiveness samples with zero unresponsive across a 0->10 container ramp, and Gate 5 tears down 10 containers with a 5.27s exit. Gates: cargo workspace 120, tauri host 40 (+3), npm 139 (+2), check 354 files 0 errors 0 warnings.
… (D-2) `discoverFrom` computed `grantedCapabilities` per plugin (loader.ts:140) and nothing ever passed it to the bridge, so `init` shipped the full host set to every view-plugin regardless of what its manifest requested. Observed live in both the dev and packaged smokes: a plugin whose manifest asks only for `log-append` was also granted `real-launch-confirm`. The manifest's capability declaration was decorative. App.svelte now passes the discovered grant, and the bridge fallback fails CLOSED - an absent grant is `[]`, not `HOST_CAPABILITIES` - so forgetting to pass it can never again silently widen a plugin's authority. `bridge.test.ts:360` asserted the full host set as expected behaviour, which encoded the defect exactly as `loader.test.ts:52` did for D-1. That is the fifth instance of this branch's one pattern, and the second time a test actively defended the bug. Assertion corrected; three tests now pin the grant, the fail-closed default, and an explicitly empty grant. Also fixes a flake introduced earlier this session: the D-7 regression test waited on `setTimeout(0)` for an async MessagePort delivery and was intermittently red (1 in 3 isolation runs). It now waits on the condition with a 5s ceiling - 8/8 clean. A ratchet that flakes is a ratchet that gets deleted. Gates: npm 142 passed (+3), check 354 files 0 errors 0 warnings, cargo workspace 120, tauri host 40.
Packaged smoke (Part 2) — run for the first time, and it moved the branchPart 1 ran on 2026-08-15. Part 2, the packaged pass, had never once been executed. It has now been run end to end on Windows against a freshly built artifact, operator-driven, with Result: 9 PASS · 2 BLOCKED · 2 NOT RUN · 0 FAIL. Full record in What this run established
Still open, deliberately
The finding worth acting onD-1, D-2, D-7 and D-8 are one defect shape: exercised on the dev path, never wired or asserted on the path that ships. All four passed every automated gate, because nothing in CI bundles the app or navigates the iframe. Twice a test actively defended the bug — Both fixes here ship as ratchets rather than notes: Gates: |
Two fixes, one mechanical and one factual. rustfmt: `packaged_plugin_root.rs` was committed unformatted and failed the `fmt + clippy` gate. Clippy was clean on both workspaces; formatting was the only failure. The factual one matters more. Commit c16356a's message and the results record both claimed "nothing in CI bundles the app". That is false, and CI proved it by running the job: .github/workflows/ci.yml:311 runs `npm run tauri build` on windows, macos and ubuntu and uploads the bundles as artifacts, on every run. The real gap is narrower and cheaper to close: nothing asserts what is INSIDE the bundle, and nothing runs the artifact. D-8 did not slip past a missing build - it slipped past a SUCCESSFUL one. `tauri build` packaged an app with no plugin root and exited 0, because a green build means "it compiled and packaged", never "it packaged the right files". Since the bundles already exist as CI artifacts, a job that unzips one and asserts the plugin root and SDK are present would have caught D-8 on the commit that introduced it. This guard closes the config half; the artifact half is the highest-value remaining addition to docs/testing/PLAN.md. The claim in c16356a's message cannot be edited now that it is pushed; this commit and the results record carry the correction.
Correction to my summary aboveI wrote that "nothing in CI bundles the app or navigates the iframe". The first half is false, and CI demonstrated it minutes later by running the job.
The real gap is narrower, and cheaper to close. Nothing asserts what is inside the bundle, and nothing runs the artifact. D-8 did not slip past a missing build — it slipped past a successful one. Since the bundles already exist as CI artifacts, a job that unzips one and asserts Corrected in Also in |
…rds reconciled The interactive smoke is finished. Part 1 (run 2, dev) and Part 2 (run 3, packaged) are both complete and #49 is READY FOR REVIEW with 18/18 CI checks green. It needs a human merge decision, not more work. Five defects closed this session: D-7 (view-plugins got no state), D-8 (the packaged bundle shipped no plugin root, so no shipped build could load a view-plugin), D-2 (every plugin granted every capability), D-4 (re-verified packaged), and D-5 (investigated, did not reproduce). Reconciled three records that contradicted each other: - docs/testing/PLAN.md — added a §1 Reconciliation block (it claimed CI has "exactly three jobs"; CI now runs nine checks), struck through GAP-006 / GAP-008 / GAP-010 / GAP-012 which runs 2 and 3 verified, and added GAP-132 as rank 1. Entry `status` fields were deliberately left `open`: status is human-owned and retirement belongs to a real scan, not a hand edit. A full testing-plan re-run is now next step 3. - docs/STATUS.md — State summary rewritten in place, session-log entry appended, Next steps re-ordered around merging #49 and filing D-3. - CLAUDE.md — pickup block rewritten; every trap in it was stale. GAP-132 is the important addition. It records the pattern behind five defects and corrects a claim I made and propagated: "nothing in CI bundles the app" is FALSE. ci.yml:311 runs `tauri build` on three OSes. Nothing looks inside the bundle, which is how D-8 passed a *successful* build - so the fix is cheap (unzip a CI artifact, assert two files) rather than architectural. Confirmed NOT problems, contrary to earlier session notes: .embargo-guard.local.json is gitignored (.gitignore:35), and PR #50 is merged.
Lands the cockpit plugin runtime: view-plugins (sandboxed iframe + MessagePort) and app-plugins
(native child webview), unified into one shell switcher.
Built by the plugin-runtime swarm on 2026-07-16/17 per
docs/SWARM-HANDOFF-plugin-runtime.md, thenleft unopened. Reopened and re-verified against current
main.What's in it
dc37806):store.svelte.tscommand-sink extraction,bridge.ts(
PluginBridge/PluginSessionover MessagePort + command policy + flood/rate kill),loader.ts,cockpit/plugin-sdk/,plugins/reference/.e3a688f):src-tauri/src/plugins/*extensions, Tauriunstablepinned
=2.11.2,capabilities/default.jsonapp::*capability, Audience proving manifest +dev-list discovery.
cda3cc1): sole writer of the three shared files. Host CSP intauri.conf.json; newview_plugins.rs(productionccplugin://asset handler); newembedding.rs(plugin_show/plugin_hide/plugin_set_rect, async, park-off-screen, warm-poolLRU cap 3);
lib.rsregistration; one topbar switcher inApp.svelte.Both spike findings are baked in rather than merged: P3's async-command fix (sync deadlocks webview
creation) and P4's opaque-origin CORS fix (
script-src … http://ccplugin.localhost, ACAO*, CSPas a response header). The spike harnesses themselves stay unmerged as throwaway, per the handoff.
Automated gates — re-verified against current
mainMerges conflict-free (27 files). Re-run today, not copied from the original swarm record:
cargo test(cockpit/ui/src-tauri)npm test(cockpit/ui)npm run checkIdentical to the numbers Lane S recorded on 2026-07-17, so three weeks of
mainmovement has notdisturbed it. Coverage includes the bridge handshake at 100× zero-drop, command policy/flood-kill,
single-sink store, and the
App.overlay/Switcher/ApprovalOverlayops-grid regression canaries.⛔ Merge blocker — interactive smoke not yet run
Draft on purpose. The automated gates cannot assert the compositing and packaging behaviour; that
needs a watched window on the target machine. Full checklist in
spikes/SPIKE-RESULTS.mdunder"REMAINING HUMAN GATE". In short — dev run: switcher shows FLEET/PROJECTS/REFERENCE/AUDIENCE; the
reference view-plugin completes
plugin-hello→readywith a policedlaunchround-trip and nonetwork; the Audience app-plugin's native webview stays glued on resize, parks off-screen when a
host overlay opens, restores on close, and leaks no orphaned webview when switching away and back;
Vite HMR still works under the new host CSP. Then the same again in a packaged build
(
npm run bundle), which is the only way to proveccplugin://resolves without a dev server.Record PASS/FAIL in
spikes/SPIKE-RESULTS.mdand mark this ready for review once it passes.