feat(plugins): declare 7 at launch, detect all 12 (#53) - #65
Merged
Conversation
The plugin *engine* landed in 3da2170 — src/core/plugins.ts parses the spore:plugin:* provenance tag and validates launch declarations, and src/aws/userdata.ts writes /etc/spawn/plugins.json. None of it was reachable. LaunchInput had no `plugins` field, so `spec.plugins` could not be set from SpawnClient, the CLI, or the UI, and nothing called detectPlugins(). Both halves of the issue's two-column matrix existed as dead code. This wires the surface. Declare at launch (7 of 12) - LaunchInput.plugins → LaunchSpec → user-data, verified against the real payload: [{"ref":"jupyterlab"},{"ref":"mountpoint-s3",…}]. - CLI `--plugin ref[@Version]`, repeatable, matching Go's pflag StringArray (cmd/launch_flags.go:351). - Dashboard launch form: a checkbox per declarable plugin, so the offered set IS the supported set and a rejection can't be reached from the form. The five that aren't offered are named with the reason underneath — an unexplained absence reads as an arbitrary omission, and worse, as "the browser has no plugin support". Detect (12 of 12) - `status` renders decoded provenance; `--plugins` asks explicitly. - Instance cards get a `plugins —` line with three distinct styles, so verify=none (a finding), verify=signature (clean) and verify=unknown (no data) can't be read alike. - spore-sync and tailscale render exactly like the rest. A card built on the install column alone shows nothing when five things are deployed, which is the defect the second column exists to fix. Two deliberate divergences from Go - A ref that can't be honoured aborts the launch. Go writes any ref into plugins.json and lets a push-dependent plugin park at StatusWaitingForPush on the box (pkg/pluginruntime/runtime.go:62) — a failure invisible from the launch side. Nothing is billed at the point of refusal and the fix is a one-word edit: refusing costs a retry, launching costs an instance that can't do its job. - The success line says "declared", never "installed". spored installs at boot; whether it worked is only knowable later from the tags. Parser fix, not a workaround parseArgs' flags map is single-value/last-wins, so `--plugin a --plugin b` would have installed one and dropped the other with no signal. Added ParsedArgs.lists + flagList(); flags[] stays last-wins, so every existing single-value reader is untouched. Both exported. Honesty rules carried through from #63/#56 - With no plugin tags, status says NOTHING (and the card renders no line). The tag is best-effort and is never written for launch-time declarations, so absence means "we don't know". --plugins gets describePluginState's full caveat rather than a bare "none". - An unparseable tag value still reports the plugin as deployed — the tag's existence is the evidence. 451 → 538 tests. Every new guard was proven against the absent behaviour first: five mutations (last-wins lists, silent-filter instead of refusal, absence-as-"no plugins installed", one class for every verify tier, fd.get instead of getAll, unregistered --plugins) produced 21 failures across four suites. Found by driving the built CLI, not by the suite: a bare unparseable token was joined into the same comma list as the decoded fields, so "verify=unknown, garbage-no-kv" read as though the garbage had been parsed. It now gets its own line; a forward-compatible key=value pair still goes inline, because that one IS provenance. Also drove the dashboard in real Chromium (new dev-only demo/direct/plugins.harness.html — the tags can only be written by the Go CLI or spored, so no path through the app itself reaches that state) and looked at the rendered card: the three verify tiers compute to green / yellow / grey and the picker reads legibly. Not ported, deliberately: `spawn plugin install` on a running instance (SSH tunnel to :7777 — no SSH in a browser), and the local halves of spore-sync / rclone / tailscale / github-actions-runner / globus-personal-endpoint. docs/data-movement.md already classified those; its `validateDeclarations` example now matches the shipped behaviour instead of implying a partial launch. Closes #53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #53.
The issue's framing: "spawn-ts has zero plugin support today … That reads as 'plugins are out of scope for the browser', which is wrong in both directions."
Half of that turned out to be already-solved-but-unreachable. The engine landed in
3da2170—src/core/plugins.tsparses thespore:plugin:*provenance tag and validates launch declarations, andsrc/aws/userdata.tswrites/etc/spawn/plugins.json. ButLaunchInputhad nopluginsfield, sospec.pluginscould not be set fromSpawnClient, the CLI, or the UI, and nothing calleddetectPlugins(). Both columns of the matrix existed as dead code. This PR is the surface.Declare at launch (7 of 12)
LaunchInput.plugins→LaunchSpec→ user-data. Verified against the real generated payload, not just the API:[{"ref":"jupyterlab"},{"ref":"mountpoint-s3","config":{"bucket":"my-data"}}]lands in/etc/spawn/plugins.json, byte-compatible with what spored'sloadPluginDeclarationsreads.--plugin ref[@version], repeatable, matching Go's pflag StringArray (cmd/launch_flags.go:351).Detect (12 of 12)
statusrenders decoded provenance;--pluginsasks explicitly.plugins —line with three distinct styles, soverify=none(a supply-chain finding),verify=signature(clean) andverify=unknown(no data) can't be read alike.spore-syncandtailscalerender exactly like the rest. A card built on the install column alone shows nothing when five things are deployed — which is the defect the second column exists to fix.Two deliberate divergences from Go
A ref that can't be honoured aborts the launch. Go writes any ref into
plugins.jsonand lets a push-dependent plugin park atStatusWaitingForPushon the box (pkg/pluginruntime/runtime.go:62) — a failure invisible from the launch side. Nothing is billed at the point of refusal and the fix is a one-word edit: refusing costs a retry, launching costs an instance that can't do the job it was launched for.```
$ spawn launch bad --ttl 4h --plugin tailscale --plugin nonesuch
launch: 2 plugins cannot be declared at launch — nothing was launched.
"tailscale" cannot be declared at launch: its local half mints an auth key and pushes it, …
"nonesuch" is not a known launch-declarable plugin. Declarable: cloudwatch-agent, …
```
validateDeclarationsstill returns rejections rather than throwing — the reason is data a caller may render or aggregate. The policy lives inSpawnClient.launchand the CLI.The success line says "declared", never "installed". spored installs at boot; whether it worked is only knowable later from the tags. A launch message must not report an unobserved outcome.
Parser fix, not a workaround
parseArgs'flagsmap is single-value/last-wins, so--plugin a --plugin bwould have installed one and dropped the other with no signal. AddedParsedArgs.lists+flagList();flags[]stays last-wins, so every existing single-value reader is untouched. Both exported for embedders. The DOM has the identical trap —fd.get()returns only the first of several same-named checkboxes — so the form usesfd.getAll().Honesty rules carried through from #63/#56
statussays nothing (and the card renders no line). The tag is best-effort and is never written for launch-time declarations, so absence means "we don't know", not "no plugins installed".--pluginsgetsdescribePluginState's full caveat rather than a bare "none".Verification
503 → 538 tests, 30 files.
npm run typecheckclean on both tsconfigs;npm run buildclean.Every new guard was proven against the absent behaviour first. Five mutations produced 21 failures across four suites: last-wins lists (7); silent-filter instead of refusal (4); absence-as-"no plugins installed" + dropped caveat + one CSS class for every verify tier (4); dropped
escapeHtml+fd.getforfd.getAll(2); unregistered--pluginsboolean + "installed" wording (4).Found by driving the built CLI, not by the suite: a bare unparseable token was joined into the same comma list as the decoded fields, so
docker: version unknown, verify=unknown, garbage-no-kvread as though the garbage had been parsed. It now gets its ownunrecognised in its tag:line; a forward-compatiblekey=valuepair still goes inline, because that one is provenance.Also drove the dashboard in real Chromium via a new dev-only
demo/direct/plugins.harness.html— the tags can only be written by the Go CLI or spored, so no path through the app itself reaches that state. Confirmed the seven checkboxes and the note, a 3-of-3 declared launch, absence before tags and presence after, and the three verify tiers computing to green / yellow / grey; then looked at the rendered screenshots.Not ported, deliberately
spawn plugin installon a running instance (SSH tunnel to:7777— no SSH in a browser), and the local halves ofspore-sync/rclone/tailscale/github-actions-runner/globus-personal-endpoint.docs/data-movement.mdalready classified those; itsvalidateDeclarationsexample now matches the shipped behaviour instead of implying a partial launch.