feat(spec): declare grantedPermissions on EnvironmentArtifactSchema — install-time granted set per plugin manifest id (#14865) - #14992
Conversation
…a` (#14865) One optional top-level envelope key, `grantedPermissions: z.record(z.string(), PluginPermissionsSchema).optional()` — the install-time GRANTED permission set per plugin, keyed by the plugin manifest `id`. Producer: the cloud control plane's consent-compile step. Consumer: the materialize-time loader via `PluginPermissionEnforcer.registerGrantedPermissions`. Absent (no consent record) and `{}` (consented to nothing) are never collapsed; the value shape is the strict `PluginPermissionsSchema`, so an unknown permission class is refused at the artifact door. Pins: declared key survives and round-trips; absent stays absent; `{}` stays `{}`; an unknown top-level sibling is still stripped; value schema identity; the manifest-id-vs-package_id residual risk pinned on the key's own description. Doc-only carrier sentence on `PluginPermissionsSchema`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
…e grantedPermissions docblock (#14865) check:generated proved exactly one artifact stale — content/docs/references/** — and regenerated it (--fix); authorable-surface, api-surface, export-origins and declaration-map are unchanged by the new optional key. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
…sions pins (#14865) A control that also asserts the new key survives goes red at the pre-change schema for the survival reason, so its strip half is never evaluated there. This one involves no grantedPermissions at all: green before and after the key, red only if the envelope door goes passthrough — the reading the ablation needs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 128 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 5dc5c90b8c5cdf80a9e8e5ec70e452ffd02d6826 && git checkout 5dc5c90b8c5cdf80a9e8e5ec70e452ffd02d6826
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 77a532d676283ab11babadc347f36e286c12e4fc 84ce33e9346501f3a9db332a8c589dd901051c12 && git checkout -B drift-repro 77a532d676283ab11babadc347f36e286c12e4fc && git merge --no-ff 84ce33e9346501f3a9db332a8c589dd901051c12
node scripts/docs-audit/affected-docs.mjs --json 77a532d676283ab11babadc347f36e286c12e4fc
|
…act contents (#14865) Patch round from the domain:spec seat: the docs-drift bot named content/docs/concepts/north-star.mdx via EnvironmentArtifactSchema — its "Environment Artifact" section enumerates the envelope's contents, so the new optional key belongs in that list. One bullet, in the page's register; the checksum bullet and the rest of the page are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
|
Provenance — Generated by Claude Code |
Fixes #14865
The artifact-contract half of #11333 option A (2026-08-30 「同意」) and the #13457 batch ruling (2026-09-01 「同意」): the consented four-class permission set
{ services, hooks, network, fs }rides the plugin artifact contract. This PR declares the carrier key on the envelope; cloud (#14034,repo:cloud) writes it at consent-compile time once its pin covers this; the loader half (#13457) stays behind #14034. Spec half only — nothing inpackages/metadata,packages/core, cloud, orenv-artifact-cache.ts.What changed
packages/spec/src/system/environment-artifact.zod.ts— one optional top-level envelope key, exactly as filed:grantedPermissions: z.record(z.string(), PluginPermissionsSchema).optional(). The key's docblock and.describe()name the producer (the control plane's consent-compile step, fromsys_package_installation.granted_permissions), the consumer (the materialize-time loader viaPluginPermissionEnforcer.registerGrantedPermissions), state absent (no consent record) vs{}(consent-bearing, consented to nothing) as never collapsed (no.default({})), state the key = plugin manifestid(notpackage_id) with the residual risk written out, and note the key sits besidemetadata, outside thechecksumdigest. The file-header Boundary section gains a "Consent state" bullet (the only part of this file the generated reference page renders — the lazy envelope is not walked).packages/spec/src/system/environment-artifact.test.ts— 13 schema-reachable parse pins (plus one pure control): declared key survives and round-trips the exact map; absent stays absent;{}stays{}at the map level and per plugin; the two readings are distinguishable on the parsed value; an unknown top-level sibling is still stripped (a combined control and a pure one with nograntedPermissionspresent); value schema is the samePluginPermissionsSchemaobject (identity); an unknown permission class is refused withunrecognized_keysatgrantedPermissions.PLUGIN; non-object values refused; all four classes accepted; the key's description names manifestid, rules outpackage_id, states absent vs{}(a pin on the contract text); apackage_id-shaped key parses — which is the residual risk, stated as such.packages/spec/src/kernel/manifest.zod.ts— doc-only carrier sentence onPluginPermissionsSchemapointing at the new key. No schema change; the.describe()string is untouched, so the generatedkernel/manifestreference page did not move (verified bycheck:generated).content/docs/references/system/environment-artifact.mdx— regenerated bycheck:generated --fix(+7 lines, the Boundary bullet). Never hand-edited..changeset/environment-artifact-granted-permissions.md—@objectstack/specminor.ENVIRONMENT_ARTIFACT_SCHEMA_VERSIONstays0.1: it is bumped on breaking envelope changes and this key is additive and optional — every artifact that parsed before parses identically (the 14 pre-existing pins are unchanged and green).PM hypotheses, measured
3386493fstrips it silently (ablation Leg A: the 11 survival-family pins go red at BASE while the two controls and the 14 pre-existing pins stay green). After: the declared key survives, an unknown sibling is still stripped, and a.passthrough()mutation (Leg B) reddens exactly the two sibling-strip controls.kernel/manifest.zod.tsalready sat in the envelope's closure viastack.zod.ts; a static walk ofkernel/manifest.zod.ts's 28-file relative-import closure reaches neithersystem/environment-artifact.zod.tsnorstack.zod.ts. The built CJS and ESM./system,./cloudand./kernelentries all load and expose the key.content/docs/references/system/environment-artifact.mdx.check:generatedon the rebuilt dist reported 1 of 15 stale (docs) and, after--fix, all 15 up to date; authorable-surface (+ its.base.jsonanchor), json-schema manifest, api-surface, export-origins and declaration-map are unchanged — the envelope is not in the authorable surface and adds no export.system/environment-artifactis not incheck-liveness.mts'sGOVERNEDlist, not inSPEC_ONLY_SCHEMAS, and not in the metadata-type registry (metadata-type-schemas.tshas no mention);packages/spec/liveness/has no such ledger. No row added;check:livenessgreen.manifest.zod.tshas no schema diff.@objectstack/specminor;check:changeset-no-major,check:empty-changesetandcheck:adr-0087-registrationgreen (non-breaking, so no ADR-0087 marker is owed).Verification (all on the final commit
304318e8)pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/environment-artifact.test.ts—Tests 27 passed (27)(14 pre-existing + 13 new).pnpm --filter @objectstack/spec typecheck—tsc --noEmit,check:scripts-typecheckandcheck:test-typecheck: OKall green;tsc -p tsconfig.test.json --listFileslists the edited test file (1 hit, 0 errors on the edited files), so the test-layer typecheck really read it.pnpm --filter @objectstack/spec buildthencheck:generated:All 15 generated artifacts are up to date; the rebuiltdist/environment-artifact.zod-*.d.tscarriesgrantedPermissions: z.ZodOptional...(fresh declarations, not a cache).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, merge base3386493f): 58 commands; 53 exit 0. Five refused with their own PREREQUISITE NOT MET text and are NOT MEASURED here, not red:check-test-completeness(needs a saved test-run log),lint check:doc-formula-expressions(needs@objectstack/formulabuilt),check:dual-build-cjs-loadsandcheck:type-check-debt(need the built workspace closure),check-dev-prereqs("workspace is not built — 1 unmet precondition, not a list of problems"). The whole-tree build those need is CI's run; scoped equivalent for the one package this diff touches: the built./system,./cloud,./kernelCJS and ESM entries load and expose the key../environment-artifact.zodrelatively andvitest.config.tsdeclares no alias, so no build sits in either leg; dist untouched throughout): Leg A restored the BASE schema file (on disk: 0grantedPermissionslines, hash equals the BASE blob) —11 failed | 16 passed (27), the 11 being every pin that needs the key. Leg B injected.passthrough()(on disk: 1 match, hash differs from HEAD) —2 failed | 25 passed (27), the two sibling-strip controls only. Restore viagit checkout HEAD -- PATHproven after each leg bygit hash-objectequal to the HEAD blob andgit diff HEAD -- PATHempty; the control leg on unmodified HEAD is 27/27.For the contract reviewer (Clause ② yes — a new key on a published contract face)
{}is carried by the schema exactly as ruled; nothing defaults it.id; the residual risk (manifestiddiffers frompackage_id) is documented on the key and pinned on its description, and apackage_id-shaped key still parses — the schema cannot tell the two apart, the producer owns it.grantedPermissionssits besidemetadata, so it is outside thechecksumdigest (which covers themetadatablock only). Consent state is control-plane state re-emitted per assembly rather than compiled metadata, which is consistent with the ruling's carrier reading; if integrity over the granted set is ever wanted it would be a separate decision, not this key.tsup.config.tssplitting: false), so./cloudand./systemcarry separate runtime copies of the envelope indist— the declaration-level single source (spec 双源清账 C10:EnvironmentArtifact ×3(./cloud ≠ ./system)—— 路线 A′:system 收敛为活 wire 形,cloud re-export,3 条 #4740 pin) is unaffected.#14034 remains open (the cloud write, waits on this landing and the pin bump); #13457 remains open (the consumer half, behind #14034).
Patch round (seat request) — head
84ce33e9The docs-drift bot named
content/docs/concepts/north-star.mdx(its "Environment Artifact" section enumerates the envelope's contents). One bullet added after thebuiltAt/builtWithline, in the page's register; thechecksumbullet and the rest of the page untouched. Commit84ce33e9(docs only). The 34 gate familiesdispatch-gatesderives for that path were re-run on84ce33e9: 33 green (check:doc-anchors,check:doc-authoring,check:docs-single-h1,check:nul-bytes,check:docs,check:doc-frontmatter,check:section-landing-index, and the lint package'scheck:doc-formula-expressions/check:doc-security-posture, measured green once the lint closure was built);check-test-completenessNOT MEASURED (it grades a saved test-run log). Tests and typecheck were not re-run for this commit: it changes one.mdxthat no test or tsc program reads. The Verification section above cites304318e8, the last commit touching source or tests.🤖 Generated with Claude Code
https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
Generated by Claude Code