Repository navigation
spec: declare grantedPermissions on EnvironmentArtifactSchema (record of manifest id → PluginPermissions) — the artifact-contract half of #14034 / #11333 Phase 1, which must land BEFORE cloud can write it (plain z.object strips undeclared keys at the artifact door) #14865
Description
Activity
- addedpriority:p1High: required for production / M2High: required for production / M2
on Sep 3, 2026 Triage — sweep repair + graded.
pm:queue(kept) ·pm:blocking·domain:spec·priority:p1·needs:contract-review.Verified at
origin/main— all three load-bearing premises:EnvironmentArtifactSchemais a plainz.object(environment-artifact.zod.ts:83) ⇒ the silent-stripping story is real: a key cloud writes before this lands is dropped at the artifact door, with no error. That is what makes the ordering (spec first, pin bump, cloud second) a correctness requirement rather than a preference.grantedPermissions→ 0 occurrences inpackages/spec/src. Not yet declared.PluginPermissionsSchemaexists atkernel/manifest.zod.ts:33⇒ the value shape is available and needs no new type.
p1: it is the head of a two-deep cross-repo chain — #14034 (repo:cloud) ispm:blockedon this, and #13457 is blocked on #14034 — and the direction is already ruled (#11333 option A, 2026-08-30 「同意」; #13457 batch, 2026-09-01 「同意」). ⛔ Nothing downstream can start until this lands, so grading it below the work it gates would strand both.pm:blockingapplied as the derived cache for that edge.needs:contract-reviewon the Clause ② mechanical floor — a new key on a published contract face is alwaysyes; the filer declared it and I am confirming rather than re-deriving.Scope — as filed, with three things ⛔ not to negotiate:
- ⛔ Do not collapse absent vs empty. Absent = no consent record;
{}= consent-bearing and consented to nothing. Cloud already writes that distinction and Phase 1 of #11333: wire granted_permissions into PluginPermissionEnforcer (F4) as the load-time gate #13457 clause 1.3 can only decide on it. - ⛔ Key on the manifest
id, notpackage_id— the enforcer is queried with the manifest identity (AppPluginderives it frombundle.manifest.id), and keying onpackage_idneeds a second name→package resolution path, the shape A multi-package artifact serializes its metadata twice — the flattened top level and everypackages[i]body carry the same definitions #14512's triage already rejected.⚠️ Pin the residual risk the card names: a package whose manifest id differs from itspackage_id. - ⛔ Ship the pin tests with it — declared key survives parse, unknown sibling still stripped. Without the second, nothing proves the door still strips what it should.
⚠️ This card is the spec half only. ⛔ Do not write the cloud side, ⛔ do not touchenv-artifact-cache.ts(it storesJSON.stringify(artifact)verbatim, so it carries the key for free once declared — the ruling's carrier assumption, and I confirmed the card's reasoning for it stands).✅ Dedup accepted; the zero was paired with a named pin (
655b106c) and a positive locator for the requested set.
Generated by Claude Code
zhuangjianguo commented
on Sep 3, 2026 CollaboratorMore actionsClaim: PM loop,
domain:specseat (seat post #6017), R1 of this shift
Session:session_0174WZTU6XcFcS7g2kykC53i(GitHubzhuangjianguo)
Branch:claude/issue-14865-environment-artifact-granted-permissions
Worktree:objectstack-issue-14865
Domain:domain:spec
File surface:packages/spec/src/system/environment-artifact.zod.ts(one new optional top-level envelope keygrantedPermissions: z.record(z.string(), PluginPermissionsSchema).optional()+ docblock naming producer and consumer; ⛔ absent vs{}preserved; key = manifestid) ·packages/spec/src/system/environment-artifact.test.ts(pins: declared key survives parse; absent and{}round-trip distinctly; an unknown sibling key is still stripped; the manifest-id-vs-package_id residual risk pinned as a stated assumption) ·packages/spec/src/kernel/manifest.zod.ts:19-31(doc-only carrier sentence pointing at the new key; ⛔ no schema change there) · whatever generated artifactscheck:generatedproves stale (json-schema manifest / references page for the artifact schema) · a liveness-ledger row only ifsystem/environment-artifactis a governed type inpackages/spec/liveness/*(the dev measures; a cross-repo producer is written into the note if a row exists) ·.changeset/*.md(@objectstack/specminor — a new authorable/published key). Stop on breach; explain in the report. ⛔ Nothing inpackages/metadata, no cloud-side work, noenv-artifact-cache.ts.
Container & model: S/M (the schema line is short; the pins and the ledger question are the work),mode:subagent,model: fable(CONTRACT_REVIEW_TIER).node scripts/pm/dispatch-gates.mjs --tier packages/spec/src/system/environment-artifact.zod.ts packages/spec/src/kernel/manifest.zod.tsatorigin/main9786d39(15:40Z): "no path-derived mandate: the surface hits none of the 3 declared glob(s)" + "Clause ② SUSPECT surface — a hint, not a verdict" on both paths (packages/spec/src/**); the tier is decided from the card content — a new key on a published contract face is the mechanical floor for clause ②.
Clause-②: yes
Serial constraints cleared: open-PR file scan at 15:38Z (22 open PRs, every file list read via REST) — none touchesenvironment-artifact.zod.ts,environment-artifact.test.tsorkernel/manifest.zod.ts; last commit on those files4d0d9445(#14714, 2026-09-02T21:14Z) — no same-day churn; lane siblings in flight (#14010 merge round onhook.zod.ts/ objectql; #14722 onstack.zod.ts) are file-disjoint; H17 on-hold trigger-file index (#9857, swept 13:47Z) names none of these files. DownstreamBlocked-by:edge: #14034 (repo:cloud) waits on this card (thepm:blockingcache); #13457 behind that.Decision re-read (all comments read, one — triage 5522848160): rulings in force are #11333 option A (2026-08-30 「同意」) and the #13457 batch ruling (2026-09-01 「同意」), quoted in the card body; triage's three non-negotiables (absent ≠ empty; key on manifest
id; ship both pin tests) go into the dispatch as rulings. Premise re-read onorigin/main9786d39(15:40Z):grantedPermissions→ 0 hits underpackages/spec/src(positive control:PluginPermissionsSchema→ 4 hits inkernel/manifest.zod.ts);EnvironmentArtifactSchema = lazySchema(() => z.object({atenvironment-artifact.zod.ts:83— a plain object, so the silent-strip premise stands. Premise holds.
Generated by Claude Code
zhuangjianguo commented
on Sep 3, 2026 CollaboratorMore actionsDispatch (R1 of this shift, 2026-09-03T16:05Z) —
domain:specseat,session_0174WZTU6XcFcS7g2kykC53i, seat post #6017.mode:subagent,model: fable(CONTRACT_REVIEW_TIER), size S/M, Clause ② yes. The dev leaves its ownClaim:comment below (session + branch +Clause-②: yes+ its complete file surface); the assignee stays the PM's.Dispatch text handed to the dev, in brief (the two rulings and triage's three non-negotiables are quoted verbatim and marked non-renegotiable; scope = the spec half only):
- One optional top-level key
grantedPermissions: z.record(z.string(), PluginPermissionsSchema).optional()onEnvironmentArtifactSchema, describe naming producer (cloud consent-compile) and consumer (materialize-time loader), absent ≠{}stated and pinned; key = manifestid, residual risk pinned as the documented assumption. - Hypotheses to measure: H1 plain-object strip today / declared-key survival + unknown-sibling strip after (schema-reachable pins); H2 the
PluginPermissionsSchemaimport has no cycle; H3 which generated artifacts move (check:generated --fix, never hand-edited); H4 whether the artifact type is liveness-governed (row only if so,cross-reposcope spelling); H5 the doc-only carrier sentence inmanifest.zod.ts; H6 changeset minor. - Landing: draft PR with
Fixes #14865+needs:contract-reviewon the PR in the same stroke (the card already carries it — dual carrier); ⛔ no ready flip. In-seat contract review at tier by this seat on delivery.
Generated by Claude Code
- One optional top-level key
Claim: os-dev subagent for #14865 (dispatched by the
domain:specseat, seat post #6017, 2026-09-03T16:05Z)
Session:session_0174WZTU6XcFcS7g2kykC53i(GitHubzhuangjianguo)
Branch:claude/issue-14865-environment-artifact-granted-permissions(pushed empty at BASE3386493f)
Worktree:objectstack-issue-14865
Clause-②: yes
Mode:mode:subagent,model: fable(CONTRACT_REVIEW_TIER)File surface (complete):
packages/spec/src/system/environment-artifact.zod.ts— one new optional top-level envelope keygrantedPermissions: z.record(z.string(), PluginPermissionsSchema).optional()with a describe/docblock naming the producer (cloud consent-compile) and the consumer (the materialize-time loader), stating absent vs{}; key = the plugin manifestid.packages/spec/src/system/environment-artifact.test.ts— schema-reachable parse pins: the declared key survives and round-trips; absent stays absent;{}stays{}; an unknown top-level sibling is still stripped (positive control); the value shape is enforced through the strictPluginPermissionsSchema; the manifest-id-vs-package_idresidual risk pinned as the documented assumption on the key's meaning.packages/spec/src/kernel/manifest.zod.tslines 19-31 — a doc-only carrier sentence pointing at the new key; no schema change there.- Whatever generated artifacts
check:generated --fixproves stale afterpnpm --filter @objectstack/spec build(json-schema manifest shard / references page / api-surface if the exported type surface moves) — never hand-edited. .changeset/environment-artifact-granted-permissions.md—@objectstack/specminor.packages/spec/liveness/*— a row only ifsystem/environment-artifactmeasures as a liveness-governed type (measured in-run; reported either way).
Out of scope, not touched:
packages/metadata,packages/core, any cloud-side work,env-artifact-cache.ts.Premise re-read on
origin/main3386493f(16:10Z):EnvironmentArtifactSchema = lazySchema(() => z.object({atenvironment-artifact.zod.ts:83(a plain object, so the silent-strip premise stands);grantedPermissions0 hits underpackages/spec/src(positive control:PluginPermissionsSchemapresent atkernel/manifest.zod.ts:33,.strict()); the card body ends with itsRefs:line (not truncated); all three comments read (triage 5522848160, PM claim 5528213556, dispatch 5528286028). Premise holds.Generated by Claude Code
Generated by Claude Code
- added 4 commits that reference this issue
on Sep 3, 2026 os-dev-report
{ "issue": 14865, "status": "done", "branch": "claude/issue-14865-environment-artifact-granted-permissions", "pr": "https://github.com/objectstack-ai/objectstack/pull/14992", "head": "84ce33e9 (BASE 3386493f; commits 7b05b01b feat, d4437273 docs regen, 304318e8 pure control test, 84ce33e9 north-star patch round)", "premise_still_valid": true, "summary": "Declared one optional top-level envelope key, `grantedPermissions: z.record(z.string(), PluginPermissionsSchema).optional()`, on `EnvironmentArtifactSchema`, with a docblock and describe that name the producer (cloud consent-compile, from `sys_package_installation.granted_permissions`) and the consumer (the materialize-time loader via `PluginPermissionEnforcer.registerGrantedPermissions`), state absent (no consent record) vs `{}` (consented to nothing) as never collapsed (no default), key = plugin manifest `id` (not `package_id`) with the residual risk written out, and note the key sits beside `metadata`, outside the checksum digest. 13 schema-reachable parse pins plus one pure sibling-strip control; a doc-only carrier sentence on `PluginPermissionsSchema`; the generated references page regenerated by `check:generated --fix` (the only generated artifact that moved); the seat's patch round added one bullet to `content/docs/concepts/north-star.mdx`; changeset `@objectstack/spec` minor. Premise verified at `origin/main` 3386493f (BASE): plain `z.object` at environment-artifact.zod.ts:83, 0 hits for `grantedPermissions` under packages/spec/src, `PluginPermissionsSchema` strict at kernel/manifest.zod.ts:33, card body ends with its Refs line, all comments read. Hypotheses: H1 holds (ablation in `tests`); H2 no cycle — 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, and the built ./system, ./cloud and ./kernel entries (CJS and ESM) load and expose the key; H3 exactly one generated file moved (content/docs/references/system/environment-artifact.mdx, +7) — authorable-surface and its .base.json anchor, json-schema manifest, api-surface, export-origins and declaration-map unchanged (the envelope is lazy, not authorable, adds no export); H4 not liveness-governed (absent from GOVERNED, SPEC_ONLY_SCHEMAS and the metadata-type registry; no ledger file) — no row added, check:liveness green; H5 comment-only, no schema diff in manifest.zod.ts, its describe untouched so the kernel/manifest reference page did not move; H6 minor, changeset gates green (non-breaking, no ADR-0087 marker owed). ENVIRONMENT_ARTIFACT_SCHEMA_VERSION stays 0.1 (additive optional key). Notes for the seat: my REST writes land as `claude[bot]` (App installation token), not the seat's login — the Claim comment is 5528311175 (posted 15:49Z before the first edit); `needs:contract-review` was hung on the PR at creation per the dispatch, before the patch-round message arrived, and no label was written afterwards; dispatch-gates warned STALE TREE (8+ commits behind origin/main at derivation) — no merge of main was done (the PM's open-PR scan read the files as disjoint; the queue rebuilds on main).", "tests": "vitest `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/environment-artifact.test.ts` on 304318e8 (the last commit touching tests or source; 84ce33e9 is one .mdx): `Tests 27 passed (27)` = 14 pre-existing + 13 new. `pnpm --filter @objectstack/spec typecheck` on 304318e8: `tsc --noEmit`, check:scripts-typecheck and `check:test-typecheck: OK` all green; `tsc -p tsconfig.test.json --listFiles` lists src/system/environment-artifact.test.ts (1 hit) and 0 errors mention the edited files. Reverse verification, fix committed first, source-resolved (the test imports ./environment-artifact.zod relatively and packages/spec/vitest.config.ts declares no alias — no build in either leg, dist untouched throughout): Leg A restored the BASE schema file with `git checkout BASE -- PATH` (on disk: grep -c grantedPermissions = 0, hash-object equals the BASE blob 6ae63da0) — `11 failed | 16 passed (27)`, the 11 being every pin that needs the key, the 16 = 14 pre-existing + the pure control + absent-stays-absent; Leg B injected `.passthrough()` with perl (on disk: 1 match, hash 0ce2c7a1 differs from the HEAD blob 09ce094c) — `2 failed | 25 passed (27)`, exactly the two sibling-strip controls; restore via `git checkout HEAD -- PATH` (absolute path, trap on EXIT INT TERM) proven after each leg by hash-object == HEAD blob 09ce094c and `git diff HEAD -- PATH` empty (porcelain 0 lines); control leg on unmodified HEAD `27 passed (27)`. Direction observed = direction predicted for both legs. The rebuilt dist on 304318e8 carries `grantedPermissions: z.ZodOptional...` in dist/environment-artifact.zod-*.d.ts (fresh declarations, not a cache). eslint --no-inline-config on the 3 edited TS files: exit 0 (a scoped reading, not the repo-wide run).", "gates": "Derived on the actual diff, no paths (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`; first derivation at 7b05b01b gave 58 commands, the re-derivation after the docs-regen commit gave 76 — the 18 docs families were run as a delta and `--ran` on the final list reads `76 derived, 76 run, 0 UNRUN`; every exit code captured before any pipe, one log per gate). On 304318e8: 71 green by their own verdict lines, among them `check:generated` after the rebuild `All 15 generated artifacts are up to date`, check:api-surface, check:authorable-surface, check:docs, check:export-origins, check:dual-source-exports, check:exported-any, check:liveness, check:strictness-ledger, check:skill-examples (`256 prose examples type-check across 3 surface(s)` after building the client-react closure), check:changeset-no-major, check:empty-changeset, check:adr-0087-registration, check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage. NOT MEASURED, each by its own PREREQUISITE NOT MET / not-built text (none a finding): check-test-completeness (grades a saved turbo test log), check-dev-prereqs (66 of 67 packages have no dist — whole workspace not built), check:dual-build-cjs-loads and check:type-check-debt (need the built workspace closure). Patch round on 84ce33e9: the 34 families dispatch-gates derives for content/docs/concepts/north-star.mdx (all already in the union) re-run — 33 green including check:doc-anchors, check:doc-authoring, check:docs-single-h1, check:nul-bytes, check:docs, check:doc-frontmatter, check:section-landing-index, and lint check:doc-formula-expressions / check:doc-security-posture (measured green now that the lint closure exists); check-test-completeness NOT MEASURED. CI status at report time: in_progress (not polled).", "deviations": "(1) Whole-tree gates needing the built workspace (check-dev-prereqs, check:dual-build-cjs-loads, check:type-check-debt) and the test-log grader (check-test-completeness) NOT MEASURED locally — CI's run; scoped equivalent for the one package this diff touches: the built ./system, ./cloud and ./kernel entries load in CJS and ESM and expose the key. (2) Downstream consumer typecheck beyond @objectstack/spec NOT MEASURED locally (the prefix direction `...@objectstack/spec` is the whole repo): the change is an additive optional key on an inferred type — widening only — and the two named consumer sites (packages/metadata/src/plugin.ts spread-parse; packages/core's `PluginPermissions as GrantedPermissions` alias) reference no envelope key by name; the required TypeScript Type Check job covers it. (3) Repo-wide `pnpm lint` not run (CI's); the 3 edited TS files were linted, reported as a scoped reading, not as a narrowed measurement. (4) vitest/typecheck not re-run on 84ce33e9: that commit changes one .mdx no test or tsc program reads. (5) Five dist-reading spec gates went red once on a re-run because dist's mtime was older than src after the test commit and the ablation checkouts; rebuilt on 304318e8, all green — completeness, not a finding. (6) PR body PATCHed once to record the patch-round head (the platform normalises the session footer to bare on edit; attribution lives in the body prose). (7) `check-half-states` ran twice as derived (371 s and 380 s, 100+ REST reads each) — see findings.", "files_changed": [ "packages/spec/src/system/environment-artifact.zod.ts (+63): import PluginPermissionsSchema; the key with docblock + describe; header Boundary bullet", "packages/spec/src/system/environment-artifact.test.ts (+146): 13 pins + 1 pure control under `grantedPermissions — install-time granted set per plugin manifest id (#14865)`", "packages/spec/src/kernel/manifest.zod.ts (+8): doc-only carrier sentence on PluginPermissionsSchema; no schema change", "content/docs/references/system/environment-artifact.mdx (+7): regenerated by check:generated --fix, never hand-edited", "content/docs/concepts/north-star.mdx (+4): one bullet in the Environment Artifact contents list (seat's patch round)", ".changeset/environment-artifact-granted-permissions.md (+47): @objectstack/spec minor" ], "mcp_calls": "0 — no mcp__github__* call was made; the card body and all comments were read via the repo-scoped REST endpoints (probe GET /repos/.../issues/14865 answered 200 at 16:08Z — channel open for this session), the Claim comment, PR creation, the one label add and this report went via REST POST/PATCH, everything else via git.", "open_questions": [ { "question": "`grantedPermissions` sits beside `metadata`, so it is outside the `checksum` digest (documented as the digest of the canonical `metadata` serialization only). Is integrity over the granted set wanted on the envelope? Informational for the contract review; not blocking.", "options": [ "A — no change: consent state is control-plane state re-emitted per assembly, consistent with the ruling's carrier reading; the fact is documented on the key, in the header Boundary bullet and on north-star", "B — a separate later decision: widen the digest or add a second digest over the granted set; not this key" ], "recommendation": "A, because the ruling places the set on the envelope as a carrier and the checksum contract as written is over `metadata`; changing digest semantics is its own contract decision with its own consumer (the loader's checksum verification) and belongs to no card in this chain." } ], "out_of_scope_findings": [ "NOT FILED — handed to PM for filing (PM tooling; the dedupe channel was deliberately not spent): scripts/pm/dispatch-gates.mjs names scripts/pm/check-half-states.mjs in its `declared WHOLE-TREE and named on every card` set; it is a report-only board patrol (its own footer: `Report-only: findings are patrol input, not a gate verdict`) that took 371 s and 380 s and 100+ REST reads per run here, so every dev card that runs the derived family pays it with no verdict to read. Suggest excluding report-only patrols from the per-card family or marking them PM-only.", "Observation, not filed: the built ./cloud and ./system entries carry separate runtime copies of the envelope schema (tsup.config.ts `splitting: false`, per-entry self-contained bundling, `#8133 stays on hold` in that file) — cross-entry runtime identity was never a property of dist, and the declaration-level single source (#4740 pin) holds. Pre-existing, unchanged here." ] }Generated by Claude Code
Generated by Claude Code
zhuangjianguo commented
on Sep 3, 2026 CollaboratorMore actionsContract review — PASS (in-seat, at tier) · ACCEPT — PR #14992 (head
84ce33e9,Fixes #14865).domain:specseat,session_0174WZTU6XcFcS7g2kykC53i(seat post #6017), 2026-09-03T16:58Z. Fuse:last_served_model = claude-fable-5-1=CONTRACT_REVIEW_TIER(read 15:29Z). Provenance: the 2026-08-31 ruling (in-seat contract review by the dispatching lane).Report:
os-dev-report5529148343 (status: done,premise_still_valid: true; source/test readings on304318e8, the docs patch round on84ce33e9— the final head differs from the tested head by one.mdxonly, verified by this seat:git diff 304318e8..84ce33e9=content/docs/concepts/north-star.mdx+4). Dev claim 5528311175 (15:49Z,Clause-②: yes; posted asclaude[bot], the App-token identity — the branch is the identity, per the claim rules). Reviewed against the card, triage 5522848160, the full diff on GitHub and this seat's readings oforigin/main.① Derived judgments (Clause ② yes — a new key on a published contract face): the accept set widens by exactly one optional top-level key on
EnvironmentArtifactSchema,grantedPermissions: z.record(z.string(), PluginPermissionsSchema).optional(); the value is the strictPluginPermissionsSchemaby identity (pinned:record.valueTypeis the same object; an unknown permission class is refused withunrecognized_keysatgrantedPermissions.PLUGIN); an unknown top-level sibling is still stripped (two controls, one with nograntedPermissionspresent at all); absent ≠{}is carried by the schema — no.default({}), both readings round-trip and stay distinguishable on the parsed value; key = manifestid, with the residual risk (apackage_id-shaped key parses and is never looked up) documented on the key and pinned on its description text. Emitted shape: an artifact without the key parses exactly as before (the 14 pre-existing pins unchanged);ENVIRONMENT_ARTIFACT_SCHEMA_VERSIONstays0.1— additive and optional, correct. Public surface: no new export (api-surface,export-origins,declaration-mapunchanged; the envelope is not in the authorable surface); the only generated artifact that moves is the reference page (check:generated15/15 after--fix). Ruling fidelity — #11333 option A / #13457 batch (carrier declared on the envelope; cloud writes at consent-compile; the loader consumes at materialize) — met; triage's three non-negotiables (absent ≠ empty; manifestid; both pin tests shipped) — met.
② Semver / changeset:@objectstack/specminor — correct for a new published key;check:changeset-no-major,check:empty-changesetandcheck:adr-0087-registrationgreen (non-breaking, no marker owed).
③ Boundary flags: (a)grantedPermissionssits besidemetadata, outside thechecksumdigest — the dev's open question, answered A (no change here: the ruling places the set on the envelope as a carrier and the digest contract as written coversmetadata); the gap is filed for triage as #14993 (a security-boundary question is the maintainer's, not this PR's); (b) per-entry runtime copies of the envelope indist(splitting: false) — pre-existing, #8133 on hold; (c)check-half-statesinside the per-card gate family (371 s / 380 s and 100+ REST reads per run) — already filed as #14899 by an earlier dev, not re-filed.Checklist: draft;
Fixes #14865on line 1 (the spec half completes the card; #14034 / #13457 are named without closing keywords and stay open); scope = the claimed files plus the seat's patch-round bullet innorth-star.mdx(the drift bot named the page; this seat verified it enumerates the envelope's contents and now lists the key); nocontent/docs/releases/; NOT governed (0 of 6 paths on the final file list); trial merge of84ce33e9vsorigin/main77a532d6clean; tests 27/27 (14 + 13) on304318e8; typecheck with--listFilesproof (1 hit); reverse verification both legs measured with on-disk proof (Leg A: BASE schema restored ⇒ 11 red; Leg B:.passthrough()injected ⇒ exactly the two sibling-strip controls red; restores proven by the HEAD blob hash and an emptygit diff HEAD); gate family 76 derived / 76 run — 71 green, 5 NOT MEASURED (whole-workspace prerequisites, CI's); the 34 docs families re-run green on the patch round. Declared and accepted: downstream consumer typecheck beyond@objectstack/specNOT MEASURED locally (an additive optional key on an inferred type — widening only; the TypeScript Type Check job on84ce33e9is the reading and must be green before the flip); repo-wide lint is CI's;mcp_calls: 0— the dev read and wrote through repo-scoped REST (claim, PR creation, one label add, one body PATCH, the report), recorded, not faulted.Landing shape: NOT governed, Clause ② PASS ⇒ ordinary queue landing. Same stroke:
needs:contract-reviewcleared on both carriers (PR #14992 + this card), each read back. Ready flip + auto-merge (squash) follow once every check on84ce33e9is green (this seat's checkpoint). On MERGED: the card closes viaFixes,pm:dispatchedis stripped,origin/mainprobed for the key. For therepo:cloudseat: #14034's unlock criterion is an installable@objectstack/specrelease carrying the key plus cloud's pin bump — not this merge (cross-repo rule); its own unlock scan reads it from there.
Generated by Claude Code
zhuangjianguo commented
on Sep 3, 2026 CollaboratorMore actionsEnqueue reading —
domain:specseat, 2026-09-03T17:24Z: PR #14992 marked ready 17:22Z, auto-merge (squash) armed 17:22:54Z (provenance 14992#issuecomment-5529474740),added_to_merge_queue17:24Z. On MERGED: the card closes viaFixes,pm:dispatchedis stripped,origin/mainprobed forgrantedPermissions; #14034 (repo:cloud) waits on an installable@objectstack/specrelease carrying the key plus its pin bump.
Generated by Claude Code
zhuangjianguo commented
on Sep 3, 2026 CollaboratorMore actionsLanding note (PM seat
domain:spec, session_0174WZTU6XcFcS7g2kykC53i, 2026-09-03T18:17Z) — PR #14992 merged through the queue at 17:59:29Z ase58ea8b3(queued 17:24Z); this card closed viaFixesat 17:59:30Z.- Probe on
origin/main(fetched 18:17Z):packages/spec/src/system/environment-artifact.zod.ts:173carriesgrantedPermissions: z.record(z.string(), PluginPermissionsSchema).optional(), with the consent-state paragraph at:36. Landed as reviewed (PASS 5529184877). - Labels: closing strips pm state —
pm:dispatchedandpm:blockingremoved with a compared readback (domain:spec, priority:p1remain, MATCH at 18:17Z).pm:blockingwas the triage sweep's cache of this card's open dependents; theBlocked-by:index on those cards is what the unlock scan reads next. - Lane inventory diff after the merge (18:17Z): no card left
openother than this one; the two queue departures ([finding] The published docs still describe the ADR-0061 search expansion as$orof$containsin six places — and one of them tells the reader the case-insensitivity question is "still open" when #7641 closed it #13988, [finding]SpecialOperatorships an empty Description column on the published reference page —$nulland$existscarry nodescribe()at all #14048) are this round's dispatches. - Downstream: the cloud consumer card ([repo:cloud] The plugin artifact contract carries granted_permissions — ruled producer half of #11333 Phase 1 #14034) consumes
@objectstack/specas a published package, so its unlock waits for an installable release carrying this key — the unlock scan's consumer-installability check, not "merged". [finding] The environment artifact'schecksumdigests themetadatablock only — the newgrantedPermissionsconsent set (#14865) rides the envelope outside any integrity coverage #14993 (thechecksumfield does not covergrantedPermissions) stays open as filed.
Generated by Claude Code
- Probe on
- added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Sep 29, 2026 - added a commit that references this issue
on Oct 7, 2026
Filed by the
repo:cloudexecution seat (seat post #6026, sessionsession_017Wu48ZSjk7HJjyJmGNBtUk, R35) as the framework half that #14034's measurement (comment on that card, 2026-09-03) showed must land first. Triage:domain:*and type are yours — expecteddomain:spec; Clause-② yes (a new key on a published contract face) ⇒CONTRACT_REVIEW_TIER. Dedup: no open objectstack card declares a granted-permissions field on the environment artifact (grantedacrosspackages/spec/srcreturns zero permission declarations at pin655b106c; the requested set ismanifest.zod.ts:290-291).Ruling this executes
#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; cloud (owner of the install-consent flow) writes it at consent-compile time, objectstack's loader consumes it from the environment-local carrier at materialize time (ADR-0003 / cloud ADR-0007:sys_package_installationis never read on a runtime path).Measured on cloud
2f469233× pin655b106c(details on #14034)sys_package_installation.granted_permissions(cloudroutes/package-install.ts:640-652, written:667/:678), preserving absent vs empty (:651-653emits it only when consent-bearing).routes/cloud.ts:747-757:schemaVersion, environmentId, commitId, checksum, metadata, functions, manifest, builtAt, builtWith, runtime) does not carry it; the marketplace-install merge loop reads the install row but consumes onlyenabled/package_version_id/with_sample_data.env-artifact-cache.ts:88-102) storesJSON.stringify(artifact)verbatim ⇒ once the envelope carries the key, the carrier carries it for free — the ruling's carrier assumption holds.registerGrantedPermissions(pluginName, PluginPermissions)(packages/core/src/security/plugin-permission-enforcer.ts:102-111), value shapePluginPermissionsSchema(packages/spec/src/kernel/manifest.zod.ts:32-44,.strict()).EnvironmentArtifactSchema(packages/spec/src/system/environment-artifact.zod.ts:83) andManifestSchema(manifest.zod.ts:158) are plainz.object, so an undeclared key written by cloud is stripped at objectstack's artifact door (packages/metadata/src/plugin.ts:900-925, parse at:908/:915). A cloud-first landing would fail silently. ⇒ spec first, pin bump, cloud second.The ask (spec half)
Declare one top-level envelope key on
EnvironmentArtifactSchema:id(recommended on [repo:cloud] The plugin artifact contract carries granted_permissions — ruled producer half of #11333 Phase 1 #14034: it is the identity the enforcer is queried with —AppPluginderives the kernel plugin name frombundle.manifest.id; keying on the control-planepackage_idwould need a second name→package resolution path, the shape A multi-package artifact serializes its metadata twice — the flattened top level and everypackages[i]body carry the same definitions #14512's triage rejected). Pin the residual risk: a package whose manifest id differs from itspackage_id.{}= consent-bearing and consented to nothing — the distinction cloud already writes and the only signal Phase 1 of #11333: wire granted_permissions into PluginPermissionEnforcer (F4) as the load-time gate #13457 clause 1.3 can decide on. ⛔ Do not collapse.manifest.zod.ts:19-31pointing at it; pin tests in the artifact schema's test file (declared key survives parse; unknown sibling still stripped).Coupling
repo:cloud, the cloud write) ispm:blockedon this card; after it lands and cloud's.objectstack-shacovers it, the cloud half is: write the map in the install loop that already holds the row, add it to the envelope literal, tests;env-artifact-cache.tsneeds no change.ArtifactKernelFactory) lives in cloud'sobjectos-runtime, so the consumer half spans both repos (objectstack owns the enforcer seam, cloud owns the call).Refs: #14034 · #11333 · #13457 · #13458 · ADR-0003 / cloud ADR-0007 · ADR-0025.