Skip to content

fix(runtime, metadata-protocol): the permission-set clone door copies a set that grants on an object this deployment does not install, and member_default's feed grants re-land - #30310

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-24iijl
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-24iijl

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

Fixes #30255
Fixes #30108

Clause-②: yes (widening) — ObjectStackProtocolImplementation.saveMetaItem's request gains one optional key, inheritedGrantObjects, reachable from @objectstack/metadata-protocol's . entry types
Metadata-protocol: none

Why main went red, measured

Run 41961254209 (797f9257), Dogfood Regression Gate (2/3), case permission-set-clone-door.dogfood.test.ts "a platform set (the lock-and-clone rule's commonest source) copies into its shipping package's pair": 422 INVALID_METADATA with two findings, objects.sys_comment and objects.sys_activity, each "no package registers an object of that name and none is saved here".

What changes

The clone door is told which grants it inherits (packages/runtime/src/domains/permission-set-clone.ts). Step 7's saveMetaItem call passes inheritedGrantObjects: inheritedGrantObjectsOf(clone), which is the keys of the copy's objects map. clonePermissionSetDefinition carries every one of them unchanged from the source.

The refusal leaves inherited grants to the source's save (@objectstack/metadata-protocol):

  • protocol-door-requests.ts: SaveMetaItemRequest gains inheritedGrantObjects?: readonly string[], documented as server-set only.
  • protocol-save.ts: the save path forwards it to the refusal. Draft and publish mode are both covered, and a migrate-stored save is still exempt as before.
  • protocol.ts and protocol-save-door-refusals.ts: unknownGrantObjectSaveRefusal takes it as a trailing parameter and drops those keys from the unknown list, after the existing "the set's current definition already grants on it" step. This is that rule applied to a copy: the grant was introduced by the source and is judged where the source introduced it. The method's docblock gains the bullet.
  • Unchanged: an unknown object the save does not say it inherits is refused exactly as before. That covers a /meta save, a re-save that adds a grant, a copy that adds one, and a draft's publish (promoteDraftForPublish and the restore doors pass no inherited set). No HTTP door forwards the key: PUT /meta builds its request field by field.

#30108 re-landed: reverting 119c2db1 (PR #30262, the revert) restores member_default's two grants, their tests and their changeset. git diff 490d136f HEAD over those five paths is empty apart from the changeset's one added bullet, which says the grant now lands with this fix. #30108's measurements are in PR #30222's body.

Changesets: .changeset/30108-member-default-feed-grants.md (restored, @objectstack/plugin-security patch, with one bullet added that names this fix) and .changeset/30255-clone-inherits-held-grants.md (@objectstack/metadata-protocol and @objectstack/runtime patch, both declaration lines).

Clause-②, measured

@objectstack/metadata-protocol's exports map has one entry, ., typed by ./dist/index.d.ts and ./dist/index.d.cts. Built on this tree, dist/index.d.ts declares SaveMetaItemRequest (a non-exported alias) with inheritedGrantObjects?: readonly string[], and the exported class ObjectStackProtocolImplementation's saveMetaItem(request: SaveMetaItemRequest) references it. So the key is reachable from the entry's type graph: a widening of the published surface. Nothing a metadata author writes changes, so Metadata-protocol: none.

Verification

On head 591198e4 (branch off main ef2ad31c), under scripts/pm/os-verify-lock.sh:

  • pnpm install exit 0, then the dogfood dependency closure build (pnpm --filter '@objectstack/dogfood^...' build) exit 0.
  • packages/qa/dogfood, run with vitest run: permission-set-clone-door.dogfood.test.ts, permission-unknown-grant-object-refusal.dogfood.test.ts, comments-permission-matrix.dogfood.test.ts, me-apps-and-everyone-baseline.dogfood.test.ts. Result: Test Files 4 passed (4), Tests 34 passed (34). This includes the case that was red on main, and the metadata: a runtime permission-set save accepts a grant on an object that does not exist (200) — the build refuses the same grant (#18202), so a typo silently grants nothing #28990 refusal's own dogfood file.
  • @objectstack/plugin-security: member-default-feed-grants.integration.test.ts, member-default-explicit-allow.test.ts, comment-reaction-own-record-floor.test.ts. Result: Test Files 3 passed (3), Tests 70 passed (70).
  • pnpm --filter @objectstack/metadata-protocol test: Test Files 312 passed, 4 skipped; Tests 22356 passed, 26 skipped.
  • pnpm --filter @objectstack/runtime test: Test Files 541 passed (541); Tests 8620 passed, 19 skipped.
  • typecheck exit 0 for @objectstack/metadata-protocol, @objectstack/runtime and @objectstack/plugin-security.
  • The derived gates (dispatch-gates --commands, 65 on this change set) and pnpm lint: their readings are in the seat hand-off.

CI does not run the dogfood shards on a pull request or in the merge queue. The runs above are this PR's dogfood evidence.

Ablations (4 legs, each restored and proven by blob hash against HEAD)

Driven through scripts/ablation-replace.mjs (WRAP mode, under os-verify-lock), on head 591198e4.

Leg Mutation Red cases
F1 the refusal ignores inherited 1: the new metadata-protocol case (the copy is refused on its inherited grant)
F2 the save path drops request.inheritedGrantObjects 1: the same case
F3 the clone door omits inheritedGrantObjects 2: the two clone-door pins that assert the save request's exact key set and the key's value
F4 inherited widened to every grant of the item 1: the new case's second half (a grant the copy adds is still refused)

The dogfood half of F3 is main's own red: with the grants landed and no fix, the clone-door case answered 422 (runs 41960290148 and 41961254209).

Full output: project file server-seat2/30255/ablations-ran.txt.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic (packages/spec not touched).
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint's reading is in the seat hand-off.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changesets.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is Chinese by the Communication rule).

Generated by Claude Code

…vity grants

This reverts the revert 119c2db, restoring 490d136 byte for byte: the
platform baseline member_default grants sys_comment (read, create, edit) and
sys_activity (read), with its tests and changeset. It lands together with the
permission-set clone door fix that the revert was waiting for.

Claude-Session: https://claude.ai/code/session_01Utp6CatT2ajve5PJf1Hhzj
Co-authored-by: Claude <noreply@anthropic.com>
… a set whose grants name an object this deployment does not install

The save door's unknown-grant refusal already leaves alone a grant the set
itself already holds. A clone's grants are the source's, copied verbatim, so
the clone door now passes the copy's grant keys to saveMetaItem as
inheritedGrantObjects, and the refusal does not judge those. An unknown object
the save does not say it inherits is still refused. member_default, which
grants sys_comment and sys_activity, copies into its package's customization
pair again on a boot without @objectstack/plugin-audit.

Claude-Session: https://claude.ai/code/session_01Utp6CatT2ajve5PJf1Hhzj
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/plugin-security, @objectstack/runtime, touching 6 documentable anchor(s).

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

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via SaveMetaItemRequest (symbol, a top-level type))
  • content/docs/permissions/system-context.mdx (via handlePermissionSetCloneWrite (symbol, a top-level function))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 42 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 bfca0efb79ac5df2049f392cdecd3bbeeea9ec61 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c66f171f0b5680a2e17282e0c8f20eb8b38a57b8 — the merge of head d770fe2150f293162ce906de2fb910370edcc6fc into base bfca0efb79ac5df2049f392cdecd3bbeeea9ec61, 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 c66f171f0b5680a2e17282e0c8f20eb8b38a57b8 && git checkout c66f171f0b5680a2e17282e0c8f20eb8b38a57b8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bfca0efb79ac5df2049f392cdecd3bbeeea9ec61 d770fe2150f293162ce906de2fb910370edcc6fc && git checkout -B drift-repro bfca0efb79ac5df2049f392cdecd3bbeeea9ec61 && git merge --no-ff d770fe2150f293162ce906de2fb910370edcc6fc

node scripts/docs-audit/affected-docs.mjs --json bfca0efb79ac5df2049f392cdecd3bbeeea9ec61

⚠️ 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 bfca0efb79ac5df2049f392cdecd3bbeeea9ec61 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 591198e4e1052010cefbce18714abc7ec526b92e
Local-runs: none
Scope: packages/metadata-protocol/src/protocol-door-requests.ts, packages/metadata-protocol/src/protocol-save-door-refusals.ts, packages/metadata-protocol/src/protocol-save.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol.unknown-grant-object-refusal.test.ts, packages/runtime/src/domains/permission-set-clone.ts, packages/runtime/src/domains/permission-set-clone.test.ts, packages/plugins/plugin-security/src/objects/default-permission-sets.ts, packages/plugins/plugin-security/src/member-default-explicit-allow.test.ts, packages/plugins/plugin-security/src/member-default-feed-grants.integration.test.ts, packages/plugins/plugin-security/src/comment-reaction-own-record-floor.test.ts, .changeset/30108-member-default-feed-grants.md, .changeset/30255-clone-inherits-held-grants.md + packages/rest/src/meta-item-write-routes.ts, packages/rest/src/meta-save-request.ts, packages/runtime/src/domains/meta.ts, packages/runtime/src/domains/packages.ts, packages/metadata-protocol/src/protocol-package-commits.ts, packages/metadata-protocol/src/protocol-authoring-gate.ts, packages/metadata-protocol/src/protocol-history-and-publish.ts, packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts, packages/runtime/src/domains/automation.ts, packages/services/service-automation/src/flow-credential-migration.ts, packages/services/service-datasource/src/plugin.ts, packages/cli/src/commands/migrate/organization-ownership.ts, packages/spec/src/api/protocol.zod.ts, packages/spec/src/api/protocol.test.ts, packages/qa/dogfood/test/permission-set-clone-door.dogfood.test.ts

Read-only review of the PR body, file list, the net diff origin/main...591198e4 (13 files, +707/−24), cards #30255 and #30108 with every comment, and the head's check-runs. Not read: the dispatch order, the seat's conclusions, the seat hand-off file. No dev report for #30255 exists on either card or on the PR at review time, so ③ reads the #30108 report (6647979158) and the PR body's own claims. One probe-free verification was done with git on the fetched ref only (blob hashes, diffs, greps); nothing was built, run or re-run.

① Derived judgments

  • Commit 1 (e7226bbc) is byte-identical to 490d136f on its five paths. Verified by blob hash: .changeset/30108-member-default-feed-grants.md 91be2f6b, comment-reaction-own-record-floor.test.ts 9cb68c48, member-default-explicit-allow.test.ts b6650923, member-default-feed-grants.integration.test.ts ac307a2c, objects/default-permission-sets.ts af3bc8d2 — each equal at 490d136f and e7226bbc. git diff 490d136f e7226bbc -- [five paths] is empty. At the head, four of the five are still identical; only the changeset moved (blob 2a40eb8b), by exactly the one added bullet that names the clone-door fix. So the re-land carries the earlier PASS record 6648282410 forward unchanged, and the ruling A rows (sys_comment read/create/edit, sys_activity read, no delete, no row policy) are as reviewed.
  • Accept-set change of commit 2 — exactly one, in one place. ProtocolSaveFacet.saveMetaItemUnaudited (protocol-save.ts:1582), in draft and in publish mode and never for a migrate-stored re-save, now hands request.inheritedGrantObjects to unknownGrantObjectSaveRefusal, which drops those keys from its unknown list AFTER the four existing filters (registry getObject by exact name, declared object row, pending object drafts, the set's own current held grants) and before composing the 422. Keys not in the inherited set are refused exactly as before; the message text does not move. No other refusal reads the key: the publish door (protocol-history-and-publish.ts:832) passes batchObjects and 'publish' and no inherited set; the restore door (protocol-authoring-gate.ts:147) passes three arguments; PUT /meta passes none. So the only save that skips the refusal for any key is one whose in-process caller states inheritedGrantObjects, and the only such caller in the tree is the clone door's step 7.
  • Which keys the clone door states. inheritedGrantObjectsOf(clone) = Object.keys(clone.objects), where clone = structuredClone(source.definition) minus PERMISSION_SET_CLONE_DROPPED_KEYS (read decorations, protection fields, packageId, managedBy, protection, adminScope, isDefault) with only name and label set. objects is not touched between the catalog read and the save, so the stated set equals the source's grant keys exactly; the clone body is closed to { name, label, package? } with any other key refused 400 before the catalog is read. The two unit pins assert the save request's exact key set and that the value equals the source's Object.keys(objects).
  • Dogfood evidence is the dev's local reading, not CI's. The Dogfood Regression Gate shards are skipped on a pull request (the aggregate job reports success with every shard skipped), so the formerly red case a platform set ... copies into its shipping package's pair is green on this head only by the PR body's 34/34 reading. The unit pin that stands in for it in CI is the new metadata-protocol case (protocol.unknown-grant-object-refusal.test.ts:368), which drives the real saveMetaItem with an unregistered object in the inherited set and asserts both the save and the refusal of a non-inherited key. Judged with what is there.

② Semver level

  • Clause-②: yes (widening) — correct, and the reachability claim holds in the tree: @objectstack/metadata-protocol's index.ts exports ObjectStackProtocolImplementation; its saveMetaItem(request: SaveMetaItemRequest) (protocol.ts:10712) is typed by the non-exported alias in protocol-door-requests.ts, which gains inheritedGrantObjects?: readonly string[]. The key is therefore in the . entry's type graph as a new optional member of a public method's parameter: a widening, nothing narrowed.
  • Metadata-protocol: none — correct: no authorable key, value or type moves; packages/spec is untouched.
  • Changeset levels: @objectstack/plugin-security patch (Clause-②: no, Metadata-protocol: none → patch); @objectstack/metadata-protocol patch and @objectstack/runtime patch (Clause-②: yes (widening) with Metadata-protocol: none → a non-protocol widening ships patch). All three follow the rule.
  • Declaration lines: the PR body's two lines are byte-identical to .changeset/30255-clone-inherits-held-grants.md's. .changeset/30108-member-default-feed-grants.md carries its own Clause-②: no line, unchanged from the reviewed landing and true for the package it versions; the body states the PR-wide maximum. Noted, not a finding: the fleet's reader takes one declaration per body and one per changeset, and each is accurate for what it covers.
  • Spec parity, observation only: the spec's SaveMetaItemRequestSchema (protocol.zod.ts:1103) and the MetadataProtocol.saveMetaItem signature (protocol.zod.ts:4512) do not declare the key. The local request type already carries two server-only keys the schema does not (source, storedParentVersion), so this follows the tree's precedent; and keeping a server-derived key OUT of the wire schema is the safer posture, since a future door that parses a body with that non-strict schema strips it. The protocol.test.ts:2566 comment that lists the implementation's parameter type as its measure is now further behind; not this PR's to move.

③ Boundary flags

From the #30108 dev report (6647979158), all re-landed unchanged in commit 1:

  1. Condition 1's premise ("member_default also reaches everyone, externals included") was measured false — org_external holds no everyone anchor and resolves external_default — reported as a corrected premise, not a fork. The integration test's block 3 pins it on the real resolver, and the ruling's intent (the grant widens nothing for an external) holds.
  2. enable.feeds: false gates create and re-thread, not read, before and after the grant; open question answered A (land as is) and the residual recorded in the seat's ACCEPT, not a new card. Unchanged here; still the one documented residual of ruling A.
  3. dispatch-gates --commands run without paths; 4. the teardown fixture fetched at depth 1; 5. the full build for two dist-reading gates ran outside os-verify-lock; 6. write budget 4 not 3. Process deviations with no effect on the tree.

For #30255 (this PR's second commit): the card's claim (6649260029) declared Clause-②: no — expected ... the dev re-declares if measurement says otherwise; the PR re-declares yes (widening) with the measurement, as the claim provided for. The claim's file surface named the clone door and/or the refusal plus tests and one changeset; the diff adds the refusal's wiring (protocol-door-requests.ts, protocol-save.ts, protocol.ts) and restores the second changeset — both inside "whichever the measurement names" and the re-land. Every ⛔ of the claim holds: the dogfood test is not in the file list, member_default's two rows are byte-identical, no packages/spec, ui, skills, docs/adr or .claude path. The body's pnpm lint and derived-gate readings are said to be "in the seat hand-off", which this review does not read; the head's Lint & Repo Gates success stands in for them.

④ Design judgment

Security first — can any external caller set inheritedGrantObjects? No, at this head. Every .saveMetaItem( producer outside tests was read (15 sites):

  • PUT /api/v1/meta/:type/:name, REST door (packages/rest/src/meta-item-write-routes.ts:341): the request literal is typed TransportScopedMetaRequest of the SPEC's SaveMetaItemRequest, which has no such key, so the key cannot even compile into this door; it spreads only saveOptions.request, a closed Pick of parentVersion | mode | surface built by metaSaveRequestOptions from If-Match / If-None-Match / ?mode / ?surface (meta-save-request.ts:118), never the body.
  • PUT /meta on the runtime dispatcher (packages/runtime/src/domains/meta.ts:1607): built field by field, the same closed saveOptions.request, item is data.
  • Clone door (permission-set-clone.ts:546): the one producer of the key; its body is closed (readCloneBody, unknown keys 400) and the value is computed from the set the door read from the security catalog.
  • Package duplicate and unmanaged import (protocol-package-commits.ts:768, :934), app un-hide (packages.ts:2249), flow clone (automation.ts:2482), the three stored-row migrations (protocol-save.ts:2554, protocol-packageless-overlay-migration.ts:587/:651, flow-credential-migration.ts:222), the external-table import (service-datasource/plugin.ts:243), the CLI ownership ceremony (organization-ownership.ts:905): each builds from stored rows or server values, field by field; none spreads a body or options object; none states the key.
  • No door parses a wire body with SaveMetaItemRequestSchema and forwards the result (its only non-test readers are the spec's own tests); no generic protocol[handler](body) dispatch exists in rest, runtime, adapters or the hono server; no in-tree MCP save producer exists at this head (the mcp surface is refused over HTTP and no in-process MCP caller of saveMetaItem is in the tree). The clone-door host wrapper (protocol.ts:1367) forwards arguments it is handed by server code only.

So the key is server-derived only, as the design requires, and the refusal that #28990 put in front of hand-authored sets still fronts every path a person or client can reach.

Does the clone door pass only keys the source holds? Yes (① third bullet). Should-fix S1: inheritedGrantObjectsOf reads the COPY, so the invariant rests on the copy step never gaining an edit to objects between the catalog read and the save. Derive the set from source.definition.objects instead and the invariant is structural; the two unit pins already compare against the source's keys and would hold unchanged. Not a defect today.

Is the alternative (treat optional-plugin platform objects as known) better? No. A boot without plugin-audit holds no registration for sys_comment, so "known" would need a hand-kept vocabulary of names optional plugins MAY register, maintained in the refusal; it would loosen every door (/meta, publish, restore, import) for those names, and the catch it exists for — a hand-typed grant on a name nothing serves — would stop firing for exactly the names an author is likeliest to copy by hand. The chosen rule is the refusal's own held rule applied to a copy: narrower, and it leaves the source's grant judged where the source introduced it.

Does the fix leave non-clone paths refused, and is that intended? Yes and yes. A fresh hand-authored set granting sys_comment through PUT /meta on a boot without plugin-audit is still 422, with the remedy sentence unchanged ("create the object first, or remove the grant"); a draft's publish and a restore are unchanged; a re-save keeping the grant was already exempt by held. That is the #28990 contract: the grant applies to nothing on that deployment, and the author, unlike the clone door, is adding it now.

Should-fix S2 (follow-up card, not this PR): the package duplicate door (protocol-package-commits.ts:768) and the unmanaged import (:934) also copy a stored permission set verbatim into a new name under mode: 'publish' and state no inherited set, so a stored set that grants on an object of an optional plugin not installed here (saved while it was, or imported) is refused 422 on those doors by the same rule this PR corrects for the clone. Not main-red, not in #30255's surface; the same inheritedGrantObjects mechanism applies and should be filed as its own card rather than widened into this PR.

Tests against the code they cite: the new metadata-protocol case drives the real saveMetaItem with a registry that knows qa_widget only, saves a copy inheriting qa_plugin_object, and refuses a second copy whose qa_typo is not inherited with nothing stored — both halves of the accept-set change, by code, status, path and sentence. The runtime pins assert the save request's exact key set, so the key cannot be dropped or renamed silently. The re-landed plugin-security tests are the reviewed ones.

Check-runs on this head, polled four times over eight minutes, last read at 2026-11-05T05:56Z: Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard, Test UI, Live E2E UI, Check Changeset, Check PR Size, PR-body guards success; Lint UI, Type Check UI, Build & E2E UI skipped (no ui/ path); Test Core still in progress (shard 6/8 done, seven shards running); Dogfood Regression Gate shards skipped on a pull request, as on every PR. Judged with what is there: nothing red; the one in-progress required context must be green before the queue, as always.

Implemented-by: session_01Utp6CatT2ajve5PJf1Hhzj
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: PASS

Should-fix (neither blocks): S1 derive the inherited set from source.definition.objects rather than from the copy; S2 file a follow-up card for the duplicate-package and unmanaged-import doors, which copy a set verbatim and state no inherited set.


Generated by Claude Code

…source set, not from the copy

Only a key the source already holds is ever passed to the save as
inheritedGrantObjects, whatever the copy step does to the copy's objects map
(contract review S1).

Claude-Session: https://claude.ai/code/session_01Utp6CatT2ajve5PJf1Hhzj
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

席位核对 — ACCEPT · domain:server#2 · head d770fe2150f293162ce906de2fb910370edcc6fc

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 size/l tests tooling

Projects

None yet

1 participant