You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#86 shipped the host half of capability negotiation: ctx.ui.web.capabilities describes what this pi-web supports. But the only consumer is runtime checks written inside each extension — a convention that diverges as extensions are added (see the #87 review thread: four of five first-party examples already skip it). The failure mode it leaves open is the worst kind: an extension using a newer API on an older host crashes at session_start, silently on hosts that predate the runtime-error surfacing. This is the third instance of the same disease in the extensions rework (bespoke surface plumbing → kernel; ad-hoc effect fields → typed effects list), and it gets the same cure: move the rule from N extension bodies into one structural checkpoint.
static requires declaration, readable without executing the extension
this issue
And two concerns that must not be conflated:
Crash prevention ("do not load me at all without X") → declarative requires, loader-enforced. For extensions with no meaningful fallback (pure-UI extensions like git-footer, recap, github-repo-panel, download-artifact).
Graceful degradation ("load me everywhere; I adapt") → runtime capabilities checks, per-extension by necessity. The notepad is the exemplar: it must not declare requires: contribute, because it wants to load on old hosts to keep its tool working — only the extension knows what its partial mode is.
Declaration syntax (static by construction)
The declaration must be parseable without running extension code — that is the entire point. Two forms:
Token grammar — flat strings, subset-checked against the host's capability set:
bare token → API member: contribute, update, capabilities
slot:<name>, kind:<name>, effect:<type> → from the policy-derived capability object
api:<n> → host advertises one token per supported version (api:1, later api:1andapi:2), so pure set-membership semantics cover version floors with no range arithmetic
Unknown token = unmet. Fail-closed. A requirement the host doesn't recognize is by definition unsatisfied — which means extensions using future capabilities automatically skip on every older host that has this checker, forever. That's the forward-compatibility engine of the whole design.
Enforcement point
pi-web already owns the candidate list for its extension directories (resolvePiWebExtensionPaths / additionalExtensionPaths feeding the resilient loader). The checker parses declarations there and:
drops unmet candidates before any extension code runs — no factory, no handlers;
records { path, unmet: [...] } in the loader status as a distinct skipped list (not errors — skipping is the mechanism working, not failing);
surfaces them in the settings Extensions card: "git-footer.ts skipped — requires contribute (not supported by this pi-web)" — reusing the Publish contribution API and pull invalidation #86 diagnostics surface.
Scoping consequence, worth stating: this covers pi-web extension directories only — exactly the population that uses ctx.ui.web. Regular pi extensions are untouched and no pi-core changes are needed.
Non-goals
No semver ranges (capability tokens subsume them), no maximum versions, no inter-extension dependencies, no load ordering, no gating on pi-core versions.
First-party adoption + the divergence guard
The four pure-UI examples declare // pi-web-requires: contribute (+ their slot tokens); the version note added in Make pi-web extension API discoverable to agents #87's docs becomes redundant and can be dropped.
The notepad deliberately declares nothing and keeps its runtime checks — both idioms documented side by side as the two patterns.
Contract test (extending Make pi-web extension API discoverable to agents #87's): every first-party example must either declare pi-web-requires or contain a runtime capability check. Consistency becomes machine-enforced for the corpus we control; third parties get it via docs, the authoring pointer, and the eventual skill.
Testing
Parser (both forms, malformed input), subset check incl. unknown-token fail-closed, skipped-list surfacing in /api/extensions/status and reload, contract test above, one e2e proving a requires-unmet extension produces a skipped row and no runtime error.
Relation to the roadmap
Completes the capabilities feature begun in #86; supersedes the per-extension guard ask from the #87 thread (amended there); prerequisite discipline for webviews, whose bridge versioning will be expressed as api:/capability floors in this same vocabulary. With this landed, the #82 extensions rework is done through everything except the two explicitly gated milestones (assets → #85 auth decision; webviews → threat-model review).
Open questions
Should skipped extensions be re-evaluated on /reload only, or also on upgrade detection?
Directive placement rule for single files (first N lines vs anywhere) — recommend first 30 lines, one occurrence.
Whether requires should eventually be part of the pi package manifest spec upstream (out of scope here; pi-web field until then).
Refs: #82 (kernel RFC), #86 (host capabilities + runtime diagnostics), #87 (review thread that surfaced the divergence argument).
Motivation
#86 shipped the host half of capability negotiation:
ctx.ui.web.capabilitiesdescribes what this pi-web supports. But the only consumer is runtime checks written inside each extension — a convention that diverges as extensions are added (see the #87 review thread: four of five first-party examples already skip it). The failure mode it leaves open is the worst kind: an extension using a newer API on an older host crashes atsession_start, silently on hosts that predate the runtime-error surfacing. This is the third instance of the same disease in the extensions rework (bespoke surface plumbing → kernel; ad-hoc effect fields → typed effects list), and it gets the same cure: move the rule from N extension bodies into one structural checkpoint.The model: two halves, two distinct concerns
ctx.ui.web.capabilities(frozen, policy-derived)requiresdeclaration, readable without executing the extensionAnd two concerns that must not be conflated:
requires, loader-enforced. For extensions with no meaningful fallback (pure-UI extensions like git-footer, recap, github-repo-panel, download-artifact).capabilitieschecks, per-extension by necessity. The notepad is the exemplar: it must not declarerequires: contribute, because it wants to load on old hosts to keep its tool working — only the extension knows what its partial mode is.Declaration syntax (static by construction)
The declaration must be parseable without running extension code — that is the entire point. Two forms:
Package extensions — manifest field:
{ "pi-web": { "requires": ["contribute", "slot:panel"] } }Single-file extensions — header directive in the first ~30 lines (precedent: skill frontmatter). Inert comment on any host:
// pi-web-requires: contribute, slot:panel, effect:open-panelToken grammar — flat strings, subset-checked against the host's capability set:
contribute,update,capabilitiesslot:<name>,kind:<name>,effect:<type>→ from the policy-derived capability objectapi:<n>→ host advertises one token per supported version (api:1, laterapi:1andapi:2), so pure set-membership semantics cover version floors with no range arithmeticUnknown token = unmet. Fail-closed. A requirement the host doesn't recognize is by definition unsatisfied — which means extensions using future capabilities automatically skip on every older host that has this checker, forever. That's the forward-compatibility engine of the whole design.
Enforcement point
pi-web already owns the candidate list for its extension directories (
resolvePiWebExtensionPaths/additionalExtensionPathsfeeding the resilient loader). The checker parses declarations there and:{ path, unmet: [...] }in the loader status as a distinctskippedlist (noterrors— skipping is the mechanism working, not failing);contribute(not supported by this pi-web)" — reusing the Publish contribution API and pull invalidation #86 diagnostics surface.Scoping consequence, worth stating: this covers pi-web extension directories only — exactly the population that uses
ctx.ui.web. Regular pi extensions are untouched and no pi-core changes are needed.Non-goals
No semver ranges (capability tokens subsume them), no maximum versions, no inter-extension dependencies, no load ordering, no gating on pi-core versions.
First-party adoption + the divergence guard
// pi-web-requires: contribute(+ their slot tokens); the version note added in Make pi-web extension API discoverable to agents #87's docs becomes redundant and can be dropped.pi-web-requiresor contain a runtime capability check. Consistency becomes machine-enforced for the corpus we control; third parties get it via docs, the authoring pointer, and the eventual skill.Testing
Parser (both forms, malformed input), subset check incl. unknown-token fail-closed, skipped-list surfacing in
/api/extensions/statusand reload, contract test above, one e2e proving arequires-unmet extension produces a skipped row and no runtime error.Relation to the roadmap
Completes the capabilities feature begun in #86; supersedes the per-extension guard ask from the #87 thread (amended there); prerequisite discipline for webviews, whose bridge versioning will be expressed as
api:/capability floors in this same vocabulary. With this landed, the #82 extensions rework is done through everything except the two explicitly gated milestones (assets → #85 auth decision; webviews → threat-model review).Open questions
skippedextensions be re-evaluated on/reloadonly, or also on upgrade detection?requiresshould eventually be part of the pi package manifest spec upstream (out of scope here; pi-web field until then).Refs: #82 (kernel RFC), #86 (host capabilities + runtime diagnostics), #87 (review thread that surfaced the divergence argument).