Repository navigation
feat(spec)!: retire the flat string-list arm of manifest.permissions — the structured ADR-0025 block is the only form (#13458) - #22094
Conversation
…— the structured ADR-0025 block is the only form WIP: schema narrowing with the list-form prescription, the ADR-0087 D2 conversion manifest-permissions-string-list-removed, its D3 semantic entry and step-18 rationale fragment, the liveness re-verdict, and the two drop reports reworded. Generated artifacts and tests follow. Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…ge the fixtures; regenerate references, authorable surface and liveness counts Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…st.permissions list arm Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…carries the retirement (check:future-spec-major) Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 147 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7b66a97d8b523a1d302b1d98be2467e2cad5c161 && git checkout 7b66a97d8b523a1d302b1d98be2467e2cad5c161
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 033e5c536db11e278182f321f0ae99cf10f4f630 1c3772561365e214d521f67cd2faa25caa897507 && git checkout -B drift-repro 033e5c536db11e278182f321f0ae99cf10f4f630 && git merge --no-ff 1c3772561365e214d521f67cd2faa25caa897507
node scripts/docs-audit/affected-docs.mjs --json 033e5c536db11e278182f321f0ae99cf10f4f630
|
Contract reviewServed-tier: Read at 2026-10-07T15:22Z by an isolated contract-review subagent, adopted by the dispatching seat. Inputs: card #13458 (body and all eleven comments; triage re-verification Check-runs as read. 18 completed: Auto Label, Build Docs, Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · source gates, filter — all ① Derived judgmentsAccept set — one narrowing and nothing else moved: right. Carriers of the new refusal sentence — true on each: right. Load paths that parse Reader census: right. The claim's bound — no consumer migration owed, stop if one exists at the head — holds. The dev's H1 is confirmed from The two reworded drop reports: right. The D2 conversion The step-18 chain: right. D3 Public surface: right. Both export names stay; Liveness: right. Residual prose. A sweep of the head tree for legacy-flat-list prose on GitHub content. No model identifier in the PR title or body, the four commit messages (the AGENTS model-free trailer pair), the changeset or the diff. ② Semver levelClause-②: Changeset — right for the If this PR lands before PR #22084: every sentence holds as written. The grade is right; the prescription "removed in @objectstack/spec 17" and If this PR lands after PR #22084 ( ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
✅ ACCEPT: PR #22094 at
|
…tire-manifest-permissions-legacy-arm
Fixes #13458
Clause-②: no (narrowing)
Phase 2 of the plugin-permissions ruling (option A, maintainer 「同意」, 2026-08-30, as recorded in #13458's body): the legacy flat
string[]arm ofManifestPermissionsSchemais retired by the standard retirement route (ADR-0049 enforce-or-remove, an ADR-0087 conversion, the spec-property-retirement playbook). The structured ADR-0025 §3.2 block{ services, hooks, network, fs }is now the only form a package manifest'spermissionstakes. Triage's corrections (6018679144) were applied as binding: there was no legacy reader to migrate. The two drop reports are reworded, and the liveness row is re-verdicted.What changes
packages/spec/src/kernel/manifest.zod.ts).ManifestPermissionsSchemais nowPluginPermissionsSchema, by identity. Both export names stay, and the api-surface is unchanged. The block answers a list on its own error map:Expected the plugin permission block { services?, hooks?, network?, fs? }, received a flat list., followed by the prescription. This is the bare-array patternListViewExportOptionsSchemaalready uses, built from the same exportedstrictObjectError+closedObject, with theprimehandle forwarded. The block also carriesEnvironmentArtifactSchema.grantedPermissionsvalues, so the answer is worded true on both carriers. Itsos migrate metasentence uses the house two-clause form, which names the case the conversion covers. The manifest's YAML example and the key's TSDoc and.describe()lose the list.manifest-permissions-string-list-removed(conversions/registry.ts,MAJOR_18_CONVERSIONS, order 61, inserted where its identifier sorts and defined above the entry that follows it). It is retired from the authoring load path, withretiredAfter: '17.7.0'. It deletes an all-string list from the stack'smanifestand everypackages[].manifest, and the notice carries the dropped strings. It never touches the structured block, an array of objects, the top-level ADR-0090 collection, ordevPlugins[], whose entries may be live plugin objects.manifest-permissions-string-list-retired(migrations/entries/semantic/, generated into the registry) and itsSTEP18_RATIONALEfragment (order 85).packages/spec/liveness/manifest.json).permissionsstayslive, on corrected evidence, and its four keys are drilled. See the census below. TheREADME.mdmanifest row gains one sentence, andstate-counts/manifest.mdwas regenerated (live 23 → 26).packages/runtime/src/app-plugin.tsandpackages/plugins/plugin-security/src/suggested-audience-bindings.tsnow name the flat list as the RETIRED legacy form, refused at parse. Classification, dedupe, log level and thearmlabels in the meta are unchanged, so their existing tests pass untouched. No other file in those two packages moved.content/docs/protocol/kernel/plugin-spec.mdxsaid the key "accepts either the legacy flatstring[]or" the block. That sentence was made false here and is corrected. The twostack.zod.tsdocblocks that described the list are corrected too.content/docs/references/**was regenerated.authorable-surface/kernel.jsongainskernel/ManifestPermissions:{fs,hooks,network,services}, because the identity alias now emits the block under its own def key.authorable-surface.base.jsonwas not touched.Measurements behind the premise (
origin/maine67ba80049)H1, reader census: no in-repo consumer reads the legacy arm. CONFIRMED. The census covered every spelling of a
manifest.permissions/manifest?.permissions/manifest[...]read,manifestSourcereads, and everyManifestPermissions*/PluginPermissions*importer. In-repo it finds:collectDeclaredSuggestions→classifyManifestPermissions;AppPlugin'sSECURITY_FIELDSloop). Both read the ASSEMBLEDPermissionSet[]reading and only REPORT an authoring-stage value as skipped (permissions一个键承载两个互不兼容的语义,且共用 registry 同一存储槽 —— ADR-0025 授权 vs ADR-0090 权限集集合 #18031);permission-set-name-collisionstest, an assembled-stage read;artifact-granted-permissions.ts,plugin-permission-enforcer.ts), which imports thePluginPermissionstype forgrantedPermissionsand never reads the manifest.Zero consumers act on the list. Control: the same instrument finds the two drop reports and the assembled-stage read, so the zero is about the key's readers and not about the pattern.
os plugin buildonly parses the manifest.os plugin publishuploads it whole asplugin_manifest.objectui pin
a58626c88dc8: 0 reads ofmanifest.permissions, against 91manifest.(id|name|version)reads as the control. Its only reader of the structured block is the marketplace install disclosure (PluginDisclosure), which readsservices/hooks/network/fsand shows "Requests no special permissions." when none is present. So the retired list was disclosed to an installer as requesting nothing.H2, conversion vs disposition: chose a STRIP conversion, registered. A capability string such as
system.user.readnames no service, hook, host or path, and no table maps one onto the four lists. So no conversion can carry the author's intent, and inventing grants in a consent request would be worse than dropping them. The list had zero readers, so the delete changes no load, grant or refusal: lossless for every runtime outcome. The conversion still has to exist, because a built artifact that carries a list would otherwise be refused at the artifact door. The judgement the delete cannot make is the D3 entry's.Authored census, by heuristic text scan with a lit control (it finds the five fixtures below): no manifest in
examples/,apps/,packages/,skills/orcontent/docs/writes a list. The exceptions are five@objectstack/specmanifest.test.tsfixtures, re-triaged here: one re-spelled to the block, three translated, and one deleted because an app ships no code that needs a grant. The other exception is the drop-report tests in runtime and plugin-security, which hand a list to an unparsed bundle on purpose and still pass.docs/design/marketplace-publishing.mdshows a list at the top level ofdefineStack, which is already the permission-set collection. It is noted below and was not edited.H3, the structured arm's accept set did not move. CONFIRMED. The error map only rewords the type error a non-object already raised. Pinned in
manifest-permissions-string-list.test.ts: every declared key alone, the full block and{}parse, and an unknown key and a non-string member are still refused. Themanifest-unknown-keys.test.tsdoor tests keep their exact rename and alias content. Only the envelope moved: the issue is now the block's ownunrecognized_keysat['permissions'], with noinvalid_unionaround it. Load-time enforcement of the block (it still refuses nothing) is not this card's and is unchanged.Liveness re-verdict. The row's old evidence (
collectDeclaredSuggestionsreading the legacy arm) was the misattribution #18031 corrected. The new evidence is the objectui install disclosure, with produceros plugin publish. The cloud hop, publish to the version row the disclosure reads, is NOT MEASURED:add_repoon cloud was denied from this seat. The note asks a cloud-capable seat to re-read and pin it. The re-verdict islive, notdead, because authoring the block changes what an installer is shown. It is not enforced at load, and the note says so.Changeset grade
mainhas no.changeset/pre.json: absent ate67ba80049, and re-read absent atorigin/main3d9188502ejust before opening. Changesets pre mode is not in, so per triage's release-state note (6038073833) this shipsminorwith its BREAKING banner and the ADR-0087 dispositionregistered manifest-permissions-string-list-removed, manifest-permissions-string-list-retired.@objectstack/runtimeand@objectstack/plugin-securitycarrypatchfor the reworded log text (same fixed group). The prescription names the bare published major, "removed in @objectstack/spec 17". That is whatcheck:future-spec-majorrequires of an unshipped pre-GA retirement, which shipsminorin the current major. A first draft said 18.0.0, and that gate refused it.Verification
All commands run at head
55481fe9ae, unless a line says otherwise. Exit codes were captured before any pipe.pnpm --filter @objectstack/spec build(JS + DTS) → 0. The dependency closures of runtime and plugin-security (--filter 'PKG^...', 31 packages) → 0.--filter '@objectstack/client-react...'→ 0 (prerequisite forcheck:skill-examples).pnpm --filter @objectstack/spec check:generated→ 0. All 15 generated artifacts are up to date against a dist built from this head (declaration stamp match). That coverscheck:liveness,check:api-surface(exports unchanged),check:authorable-surfaceandcheck:docs.@objectstack/spectests: the full--project localrun → 622 files / 18536 passed (at71735b5bc0; the later commit changes only the prescription's version word and its pin). Targeted at the final head:src/kernel/ src/conversions/ src/migrations/plus the migrate-sentence pin → 69 files / 1828 passed.--project reposcripts/step18-rationale-merge.test.ts scripts/conversions-major18-merge.test.ts→ 2 files / 21 passed. The newmanifest-permissions-string-list.test.ts→ 17/17.pnpm --filter @objectstack/spec typecheck(tsc + scripts + test layer) → 0.@objectstack/plugin-securityvitest → 169 files / 3640 passed.@objectstack/runtimevitest--project local→ 332 files / 4693 passed.typecheckof both (tsc +check:test-typecheck) → 0, at the final head.packages/runtime/srcassigned a string list toManifestPermissionsand toObjectStackManifest['permissions'], plus a structured block as a control.tsc -p packages/runtimeagainst the rebuilt spec.d.ts→ exit 2, exactly two TS2559 errors on the two list lines, the block line clean. The probe was removed andgit statusread clean.node scripts/pm/dispatch-gates.mjs --commandsderived 121 families, re-derived identically after the last commit. 117 → 0.check:future-spec-major→ 1 at first (the 18.0.0 wording above), fixed, re-run → 0.check:skill-examples→ 3 (prerequisite) at first, re-run after building client-react → 0, with 262 prose examples type-checked. The changeset gates (check-adr-0087-registration,check-changeset-no-major,check-empty-changeset, each with--self-test) → 0.--ranreconciliation: 119 of 121 run.check:i18n, reason: its prerequisite is the CLI plus the build closure of every package whose extract config it runs (exit 3), and this diff touches no metadata form or translated label.check:dual-build-cjs-loads, reason: it needs every package'sdist/(37+ missing, exit 3). Both are left to CI.eslint --no-inline-config --format jsonon the 10 changed.tsfiles → 10 files linted, 0 errors, 0 warnings. That is the whole changed-TS population, every one inside eslint's own file globs, with none ignored.eslint.config.mjsnever enables type-aware linting (noparserOptions.project), so this diff cannot move the verdict of an untouched file. The repo-widepnpm lintis CI's.main: the branch is at basee67ba80049.maingained3d9188502eandd4680d2820, which touch no file in this diff (their generated shards are other categories). The PR's merge ref and the queue validate the joint state.Acceptance notes
ManifestPermissionsSchemasurvives as an identity alias ofPluginPermissionsSchema. Deleting the export would be a second breaking removal. It would also regenerateskills/objectstack-platform/references/_index.md, which is Tier H and outside this card. The cost is fourkernel/ManifestPermissions:*authorable-surface rows that duplicatekernel/PluginPermissions:*. Retiring the name is a separate decision.permissionsonto the marketplace version row. Re-reading it needs a cloud-capable seat, and the row's note says so.packages/runtime/src/app-plugin.manifest-security-collision.test.ts. Its header table still describesmanifest.permissionsas the legacy flatstring[]or the structured block, and a comment saysdefineStackrefuses an object array withinvalid_union; it is nowinvalid_typewith the list answer. The tests still pass, and the dispatch fenced every other file in those packages. Carrier: none.docs/design/marketplace-publishing.md(a design doc, not published) showspermissions: ['system.object.create', …]at the top level ofdefineStack, which was already the permission-set collection before this change, and a §7.4 reviewer snippet in the same flat form. It was not made false here and was not edited. Carrier: none.@objectstack/spec, a legacy-list plugin manifest is refused at its publish gate. A stored version row that holds a list is cloud's own data and was not measured from here.Generated by Claude Code