Skip to content

feat(spec): declare grantedPermissions on EnvironmentArtifactSchema — install-time granted set per plugin manifest id (#14865) - #14992

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-14865-environment-artifact-granted-permissions
Sep 3, 2026
Merged

feat(spec): declare grantedPermissions on EnvironmentArtifactSchema — install-time granted set per plugin manifest id (#14865)#14992
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-14865-environment-artifact-granted-permissions

Conversation

@claude

@claude claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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 in packages/metadata, packages/core, cloud, or env-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, from sys_package_installation.granted_permissions), the consumer (the materialize-time loader via PluginPermissionEnforcer.registerGrantedPermissions), state absent (no consent record) vs {} (consent-bearing, consented to nothing) as never collapsed (no .default({})), state the key = plugin manifest id (not package_id) with the residual risk written out, and note the key sits beside metadata, outside the checksum digest. 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 no grantedPermissions present); value schema is the same PluginPermissionsSchema object (identity); an unknown permission class is refused with unrecognized_keys at grantedPermissions.PLUGIN; non-object values refused; all four classes accepted; the key's description names manifest id, rules out package_id, states absent vs {} (a pin on the contract text); a package_id-shaped key parses — which is the residual risk, stated as such.
  • packages/spec/src/kernel/manifest.zod.ts — doc-only carrier sentence on PluginPermissionsSchema pointing at the new key. No schema change; the .describe() string is untouched, so the generated kernel/manifest reference page did not move (verified by check:generated).
  • content/docs/references/system/environment-artifact.mdx — regenerated by check:generated --fix (+7 lines, the Boundary bullet). Never hand-edited.
  • .changeset/environment-artifact-granted-permissions.md@objectstack/spec minor.

ENVIRONMENT_ARTIFACT_SCHEMA_VERSION stays 0.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

  • H1 (plain-object strip) — holds. Before the key: the schema at BASE 3386493f strips 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.
  • H2 (no import cycle) — holds. kernel/manifest.zod.ts already sat in the envelope's closure via stack.zod.ts; a static walk of kernel/manifest.zod.ts's 28-file relative-import closure reaches neither system/environment-artifact.zod.ts nor stack.zod.ts. The built CJS and ESM ./system, ./cloud and ./kernel entries all load and expose the key.
  • H3 (which generated files move) — exactly one: content/docs/references/system/environment-artifact.mdx. check:generated on the rebuilt dist reported 1 of 15 stale (docs) and, after --fix, all 15 up to date; authorable-surface (+ its .base.json anchor), json-schema manifest, api-surface, export-origins and declaration-map are unchanged — the envelope is not in the authorable surface and adds no export.
  • H4 (liveness-governed?) — no. system/environment-artifact is not in check-liveness.mts's GOVERNED list, not in SPEC_ONLY_SCHEMAS, and not in the metadata-type registry (metadata-type-schemas.ts has no mention); packages/spec/liveness/ has no such ledger. No row added; check:liveness green.
  • H5 — done as a comment-only change; manifest.zod.ts has no schema diff.
  • H6 — @objectstack/spec minor; check:changeset-no-major, check:empty-changeset and check:adr-0087-registration green (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.tsTests 27 passed (27) (14 pre-existing + 13 new).
  • pnpm --filter @objectstack/spec typechecktsc --noEmit, check:scripts-typecheck and check:test-typecheck: OK all green; tsc -p tsconfig.test.json --listFiles lists the edited test file (1 hit, 0 errors on the edited files), so the test-layer typecheck really read it.
  • pnpm --filter @objectstack/spec build then check:generated: All 15 generated artifacts are up to date; the rebuilt dist/environment-artifact.zod-*.d.ts carries grantedPermissions: z.ZodOptional... (fresh declarations, not a cache).
  • Gate family derived on the actual diff (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, merge base 3386493f): 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/formula built), check:dual-build-cjs-loads and check: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, ./kernel CJS and ESM entries load and expose the key.
  • Reverse verification, with the fix committed first, source-resolved (the test imports ./environment-artifact.zod relatively and vitest.config.ts declares no alias, so no build sits in either leg; dist untouched throughout): Leg A restored the BASE schema file (on disk: 0 grantedPermissions lines, 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 via git checkout HEAD -- PATH proven after each leg by git hash-object equal to the HEAD blob and git diff HEAD -- PATH empty; the control leg on unmodified HEAD is 27/27.

For the contract reviewer (Clause ② yes — a new key on a published contract face)

  • Absent vs {} is carried by the schema exactly as ruled; nothing defaults it.
  • The key is keyed by manifest id; the residual risk (manifest id differs from package_id) is documented on the key and pinned on its description, and a package_id-shaped key still parses — the schema cannot tell the two apart, the producer owns it.
  • Observation, no change proposed: grantedPermissions sits beside metadata, so it is outside the checksum digest (which covers the metadata block 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.
  • Pre-existing, not changed here: the built entries are self-contained per entry (tsup.config.ts splitting: false), so ./cloud and ./system carry separate runtime copies of the envelope in dist — 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 84ce33e9

The docs-drift bot named content/docs/concepts/north-star.mdx (its "Environment Artifact" section enumerates the envelope's contents). One bullet added after the builtAt / builtWith line, in the page's register; the checksum bullet and the rest of the page untouched. Commit 84ce33e9 (docs only). The 34 gate families dispatch-gates derives for that path were re-run on 84ce33e9: 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's check:doc-formula-expressions / check:doc-security-posture, measured green once the lint closure was built); check-test-completeness NOT MEASURED (it grades a saved test-run log). Tests and typecheck were not re-run for this commit: it changes one .mdx that no test or tsc program reads. The Verification section above cites 304318e8, the last commit touching source or tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i


Generated by Claude Code

…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
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/kernel/manifest.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/north-star.mdx (via EnvironmentArtifactSchema (symbol, a top-level const))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/kernel/manifest.zod.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 77a532d676283ab11babadc347f36e286c12e4fcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 5dc5c90b8c5cdf80a9e8e5ec70e452ffd02d6826 — the merge of head 84ce33e9346501f3a9db332a8c589dd901051c12 into base 77a532d676283ab11babadc347f36e286c12e4fc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 77a532d676283ab11babadc347f36e286c12e4fc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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

Copy link
Copy Markdown
Collaborator

Provenance — domain:spec seat (session_0174WZTU6XcFcS7g2kykC53i, seat post #6017), 2026-09-03T17:22Z. Landing pre-checks at head 84ce33e9: in-seat contract review PASS + ACCEPT 14865#issuecomment-5529184877 (at tier; Clause ② yes — one optional key added to a published contract face, absent ≠ {} carried by the schema, value = the strict PluginPermissionsSchema by identity); needs:contract-review cleared on both carriers (PR + #14865) and read back; check-clause2-carriers --pair 14992 exit 0; NOT governed (check-governed-merges --test: 0 of 6 paths); every check on the head green — 40 runs, 36 success / 4 skipped / 0 pending / 0 red (Lint & Repo Gates, TypeScript Type Check, Governed Surface Queue Guard, Check Changeset among them), combined status success; mergeable_state: clean; trial merge vs origin/main clean. Fixes #14865 on line 1 (the spec half completes the card); #14034 and #13457 are named without closing keywords and stay open. Marking ready and arming auto-merge (squash) now; on MERGED the seat strips pm:dispatched on #14865 and probes origin/main for the key.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 3, 2026 17:22
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit e58ea8b Sep 3, 2026
42 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-14865-environment-artifact-granted-permissions branch September 3, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/m tests tooling

Projects

None yet

2 participants