Skip to content

Plugin manifest v3: http block, derived requires, gate-aware install refusal - #1732

Closed
michielbdejong wants to merge 1 commit into
claude/plugin-routes-gatesfrom
claude/manifest-v3-http
Closed

michielbdejong wants to merge 1 commit into
claude/plugin-routes-gatesfrom
claude/manifest-v3-http

Conversation

@michielbdejong

Copy link
Copy Markdown
Contributor

Closes #1712. Stacked on #1726 (#1711); base is claude/plugin-routes-gates.

Design: atomic-plugins docs/design/server-plugin-routes.md (ontola/atomic-plugins#108), sections 0.1, 0.4, 1, 2.2, D9, D12.

What

  • Manifest schemaVersion: 3 with an optional http block (mount, routes, wellKnown, writeTargets, keys, tokens, listeners, sidecars, reason). It lives in a new file, server/src/plugins/manifest_http.rs, mirrored by browser/lib/src/plugin-manifest-http.ts. The Manifest struct only gains one http field, so this should merge easily with Plugins: let a plugin accept a file and declare its destination (#1653) #1691 (accepts/destination) and atomic-proxy: URLs for plugins, resolved to the delegated connection on the configured proxy (#1700, answer 4) #1725 (proxy).
  • Compiled into every build that can install plugins (wasm-plugins), not behind plugin-routes, so the refusal can be precise.
  • Byte-identical v2: an empty http block is left out of the serialization. testdata/plugin-manifest/release-ids.json pins the release id of every accepted v1/v2 fixture. The ids were computed on the base branch before this change.
  • Validation:
    • Path patterns: literal segments, whole-segment {param}, a trailing {*rest}, no regex, no ./...
    • At most 32 routes, and no overlaps between routes that share a method.
    • Methods.
    • caller requires auth: atomic. On drive-prefix, only anonymous is allowed unless auth: atomic.
    • bearer requires tokens.
    • writes must name write targets. enqueues must name declared write operations.
    • Write target parents and classes.
    • Key and token names ([a-z0-9-]), key algorithms.
    • The well-known allowlist, with shared vs exclusive claims.
    • listeners only with world: server-extension.
    • Wildcard-host operations must be enqueued by some route.
    • An http block in a v2 manifest is rejected.
  • Gate: Manifest::gate() gives none | read-only | read-write, the listener and sidecar names, and each surface with its level (design 0.1).
  • Derived requires (Manifest::requires()): host-credentials, wasm-sandbox, persistent-host, public-origin, plugin-routes:<level>, operator-listener:<name>, operator-sidecar:<name>. It is sorted, and each /plugin-catalog entry now carries it.
  • Refusal: Installation activation checks the release against config.plugin_routes from Gate plugin public surfaces at build time and at runtime (AS-01) #1726 before grants and before anything is materialized. This covers install and upgrade (a change of release/releaseId/status). A refused upgrade is a refused commit, so the old release keeps running. /plugin-release-pin answers 409 application/problem+json with { type: "host-feature-unavailable", feature, needed, compiled, level, surfaces, listeners, sidecars, status, title, detail }. @tomic/lib's pinPluginRelease throws that as a HostFeatureUnavailableError. The lib also exports checkHostFeatures(manifest.http, hostFeatures.pluginRoutes), httpGate, derivedRequires and hostFeatureMessage for Catalog and install review: gated plugins and public endpoints #1713. The messages match 0.4 word for word on both sides.

Tests

  • Shared fixtures, run by both Rust and vitest:
    • http-index.json: 8 accepted cases, with the gate and requires each needs, and 32 rejected cases.
    • http-refusals.json: 12 node/manifest cases, one or more per message: not compiled, level too low (both off and read-only), listener missing, sidecar missing, plus the allowed cases.
  • accepted_fixtures_keep_their_release_ids.
  • Installation test: a gated release is refused at install, with the message for this build. A v3 manifest without http installs. An upgrade to the gated release is refused, and the stored releaseId and materialized meta stay unchanged.
  • Handler test: /plugin-release-pin returns 409 with the typed problem. A v3 manifest without http pins. The catalog shows requires.
  • Results:
    • cargo test -p atomic-server --lib: 292 passed, both with and without --features plugin-routes.
    • cargo clippy --all-targets -D warnings: clean both ways.
    • browser/lib vitest: 1005 passed.
    • browser/lib lint and typecheck, and data-browser typecheck: clean.

Design points I had to interpret

  • Where the typed problem travels. An install or upgrade is an Installation commit, and commit errors only carry a message. So that path returns the 0.4 message text. The structured problem is on /plugin-release-pin, and the lib can compute it client-side from hostFeatures.
  • surfaces lists only what exceeds the node's level. At read-only, it names only the read-write surfaces. For a missing listener or sidecar, it names only those.
  • Sidecars have no manifest shape in the design. I added http.sidecars: [{name, reason}].
  • {*rest} matches one or more segments. Overlap is checked per shared method, like the (host, method, pattern) registry key.
  • Which routes count as read-only. A route is read-only only if it is GET/HEAD, anonymous, auth: none, and has no body, writes or enqueues. Any other route, and any write target, key, token, delivery, listener or sidecar, is read-write.
  • persistent-host from cron or query triggers is not derived, because triggers aren't in the manifest. wasm-sandbox is derived from wasip2/1 or entrypoints.run.
  • Publishing to the marketplace is not refused. It is meant for other nodes. Only pin, install and upgrade are checked here.
  • Key algorithms and limits. Keys allow rsa-sha256 | ed25519. timeoutMs is capped at 30000 and inline maxBodyBytes at 1 MiB (design 2.8).
  • The wasm-plugins gate is kept. manifest.rs was already behind wasm-plugins, and a build without it can't install plugins at all.
  • An existing fixture changed. unknown-version.json moved from schemaVersion: 3 to 4, since 3 is now valid. The same applies to one app-package.test.ts case.

No UI changes (#1713 is out of scope).

🤖 Generated with Claude Code

…1712)

Manifest schemaVersion 3 adds an optional `http` block (routes,
well-known claims, write targets, keys, tokens, listeners, sidecars),
validated in every wasm-plugins build, not only behind plugin-routes, so
a node without the feature can say precisely why it refuses a release.
The block is left out of the serialization when empty; the release ids
of every accepted v1/v2 fixture are pinned in release-ids.json.

From the block the host computes the gate a release needs (none,
read-only, read-write, plus listener and sidecar names) and derives the
`requires` list (public-origin, plugin-routes:<level>,
operator-listener:<name>, operator-sidecar:<name>, ...), which
/plugin-catalog now carries per entry.

Installation activation (install and upgrade) and /plugin-release-pin
compare the release with the node's gates from #1711 and refuse with the
messages of the design's 0.4. The pin endpoint answers 409 with the
typed problem host-feature-unavailable; @tomic/lib throws it as
HostFeatureUnavailableError. A refused upgrade is a refused commit, so
the old release keeps running.

@tomic/lib mirrors validation, gate, requires and the refusal; both
sides run the shared fixtures http-index.json and http-refusals.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@michielbdejong

Copy link
Copy Markdown
Contributor Author

Merge-risk triage (2026-09-28). This is a read-only review of this PR's own diff. The rule it applies: nothing that works today may break for existing users or clients, and new behaviour is added alongside the old. The stack is being rebased onto develop as one chain before merging, so line references are against the current stacked base.

Verdict: SAFE

Manifest v3 is additive. Manifest was already deny_unknown_fields, so a v2 manifest with http was refused before too. An empty http block isn't serialized, and accepted_fixtures_keep_their_release_ids pins existing ids. The new install-time check_host_features parses the manifest the same way check_grants does, so no plugin that installs today is refused.

🤖 Generated with Claude Code

@michielbdejong

Copy link
Copy Markdown
Contributor Author

Closing as part of the plugin-host stack cleanup (Q-101): not needed by atomic-plugins now. See the inventory at #1726 (comment). The branch is kept, and pin candidates are unaffected. Closed by Claude (VPS coordinator) on behalf of Michiel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant