Repository navigation
fix(metadata-protocol)!: refuse publishing a hierarchy-scope grant when no hierarchy resolver is mounted; explain names the absent resolver (#26188) - #26305
Conversation
…olver is refused on publish, warned on a draft, and named by explain Claude-Session: https://claude.ai/code/session_01M4SCfSer74vqRR1SLn1hWP Co-authored-by: Claude <noreply@anthropic.com>
…explain cell Claude-Session: https://claude.ai/code/session_01M4SCfSer74vqRR1SLn1hWP Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4SCfSer74vqRR1SLn1hWP Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4SCfSer74vqRR1SLn1hWP Co-authored-by: Claude <noreply@anthropic.com>
…chyScopeGrants Claude-Session: https://claude.ai/code/session_01M4SCfSer74vqRR1SLn1hWP Co-authored-by: Claude <noreply@anthropic.com>
…eading depth arm; pin it through the real sharing service Claude-Session: https://claude.ai/code/session_01M4SCfSer74vqRR1SLn1hWP Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4SCfSer74vqRR1SLn1hWP Co-authored-by: Claude <noreply@anthropic.com>
…refusal first (#26188) With no hierarchy resolver mounted the metadata door refuses the unit_and_below profile (422 INVALID_METADATA, hierarchy-scope-capability-required). The open-edition block pins that, then saves the profile with the reference resolver mounted and unmounts it, so the fail-closed read is still proven for a profile stored while an enterprise resolver served the database. Claude-Session: https://claude.ai/code/session_01M4SCfSer74vqRR1SLn1hWP Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 150 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 f7c7a36c0b6ac2173a74543c1ef614f7984c722c && git checkout f7c7a36c0b6ac2173a74543c1ef614f7984c722c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 69aa630b9fd6ef20f76cbac9ed4d9b9efa69c804 1267e3cb2b5cec5d538fd5da7401a68bd0adde78 && git checkout -B drift-repro 69aa630b9fd6ef20f76cbac9ed4d9b9efa69c804 && git merge --no-ff 1267e3cb2b5cec5d538fd5da7401a68bd0adde78
node scripts/docs-audit/affected-docs.mjs --json 69aa630b9fd6ef20f76cbac9ed4d9b9efa69c804
|
Contract reviewServed-tier: Read on the net diff ① Derived judgmentsPublic-surface changes the diff implies, each judged.
Accept-set change (the narrowing), bounded exactly.
Explain (9b). The new arm sits after Shipped examples, apps and dogfood worlds. Read every world that declares a hierarchy depth: ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #26188, ! title, patch changeset for @objectstack/spec, @objectstack/metadata-protocol and @objectstack/plugin-security, with a BREAKING banner and ADR-0087 not-required (no-migration-prescription). Mergeable clean. Not governed (0 of 16 paths), 591 changed lines. |
| Fix (9a) | The rule hierarchy-scope-capability-required is gate-local in the runtime authoring gate. Its input, resolver presence, is a host fact read from the kernel service hierarchy-scope-resolver on each write. Publishing a permission with own_and_reports, unit or unit_and_below on either axis answers 422 INVALID_METADATA through all four doors (active save, per-item publish, package batch publish, restore) when no resolver is mounted. A draft save answers 200 with the same entry in advisories (D1 holds; no collection read). A mounted resolver, or a host with no service registry, is unchanged. This is the route agreed before the claim: no ledger row, no _diagnostics. delegated-admin-gate.ts and error-code-ledger.zod.ts are untouched. |
| Fix (9b) | The explain record cell gains a resolver-absent branch after the "depth admitted this row" arm, so a row the depth admitted is never misattributed. hierarchyResolverMounted is an optional dependency, and absent reads as before. |
| Spec | The one allowed helper export is findHierarchyScopeGrants, with its api-surface and export-origins rows. defineStack's refusal text is byte-identical. |
| Evidence | 40 check runs succeeded and 8 were expected skips on this head, 0 failed. There are three red legs (door, explain, draft advisory), each restored to HEAD. Package suites: metadata-protocol 21,549, plugin-security 4,929 and spec 21,965 passed. Derived gates: 99 of 99 exit 0, and --ran reconciles. The one dogfood world that published a hierarchy grant without a resolver now pins the 422 first, then saves with the reference resolver mounted. |
| Carried | The contract review's lookup-shape flag is filed as cloud-lane card #26312: the door reads the services map while sharing's lookup is three-tier, so a factory-registered enterprise resolver would read as absent. The stale advisories .describe() and the unchanged object-level depth cell are noted follow-ups and block nothing. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay. Every check on this head is green or an expected skip, and the contract review PASS is on record. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #26188
Clause-②: yes (narrowing: on a runtime with no hierarchy resolver, publishing a permission set whose grant uses
own_and_reports,unitorunit_and_belowis refused, where it used to answer 200; widened:@objectstack/spec/securitygains the exportfindHierarchyScopeGrants, and@objectstack/plugin-security'sExplainEngineDepsgains the optional memberhierarchyResolverMounted)Metadata-protocol: none
Ruling-ref: #26059 ruling Q3 / Q4 (
6416233738), items 9a and 9b of the item reading (6415971852); routes agreed with the server seat before the claim (claim6424008328)Summary
Hierarchy depths (
own_and_reports,unit,unit_and_below) are enforced only by the enterprise hierarchy resolver (capabilityhierarchy-security, ADR-0057). Without one, the open sharing service evaluates them asown.defineStackalready refuses such a grant at authoring time. A runtime save of the same permission set used to answer 200 with no word, though, and explain then blamed the caller's unit.9a, the save and publish doors.
Rule location. The check is a gate-local rule in the runtime authoring gate (
runtime-authoring-gate.ts), beside the [转移] 发布期护栏:多组织下「平台级 + schedule + create_record + 未显式 organization_id」拒绝——#6155 Q3=A 裁决的 lint 半边 #6285 and forms: on a walled tenancy posture a published public form bound to a tenant-scoped object is served (GET 200) but every anonymous submit answers 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED #21476 rules. Its input is a deployment fact, soAUTHORING_RULESmust not judge it.protocol.hierarchyResolverMounted()reads the kernel servicehierarchy-scope-resolveron every write, the same lookup the sharing plugin uses, andassertRuntimeAuthoringRulespasses it in. This is the shaperlsMembershipResolverKeyshas. It does not detect the resolver by plugin name.defineStackis that host's door.Publish, resolver absent. These all answer 422
INVALID_METADATAwith oneissues[]entry per grant, under the rulehierarchy-scope-capability-required:Each entry is located at
permissions.NAME.objects.OBJECT.readScope(orwriteScope), and its message nameshierarchy-security.Draft save, resolver absent. The save answers 200.
advisoriescarries the same entry atseverity: 'warning'.advisoriesas a "Saved, with warnings" toast.Resolver mounted. Nothing changes on either door.
Shared list and sentence.
@objectstack/spec/securityexportsfindHierarchyScopeGrants, the one list of hierarchy depths and the one finding sentence.validateHierarchyScopeCapabilitynow reads it, anddefineStack's refusal text is byte-identical to before (stack-refusal-envelopes.test.tsholds).Why the code is
INVALID_METADATA, notSTACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED.INVALID_METADATA, withissues[].rule. Every gate-local refusal ([转移] 发布期护栏:多组织下「平台级 + schedule + create_record + 未显式 organization_id」拒绝——#6155 Q3=A 裁决的 lint 半边 #6285, metadata: refuse to save or load a page whoserequiresnames a plugin that is not loaded (1 key) #20312) rides that envelope rather than adding a second envelope or a second throw site. A rule inside it reaches all four write doors above at once.STACK_*code at an HTTP door would need a provenance row under@objectstack/metadata-protocolinerror-code-ledger.zod.ts, a file this claim keeps off (sibling delegated-admin gate: scope violations answer PERMISSION_DENIED, so a client cannot tell them from an opaque denial (own registered code) #26189). That ledger row also readsdoor: 'none',defineStackonly.INVALID_METADATAis already registered for this package, so no ledger row is added. The server seat agreed this route before the claim.9b, explain.
explain-engine.ts's depth-layer record cell gains a resolver-absent branch, just ahead of the arm the card names as misleading (sharingMatches === false). It reads: "Hierarchy resolver absent — requires enterprise; depth evaluated as 'own'." The outcome isexcludedwhen the owner-match failed, andnot_evaluatedwhen a share admitted the row.ExplainEngineDeps.hierarchyResolverMountedis optional, and absent means the cell reads as before.security-plugin-explain-access.tswires it fromresolveKernelService('hierarchy-scope-resolver').Tests
Head
1267e3cb(main merged at617f27eb).runtime-authoring-gate.hierarchy-scope.test.ts(new, pure gate, 7 tests):code+status, rule, path,rulesRun;defineStack's finding sentence;own/orguntouched;protocol.runtime-authoring-gate.test.ts, 3 new rows throughsaveMetaItem/publishMetaItemand a real service registry:explain-engine.test.ts, 2 new cells: resolver absent names the resolver, not the depth; resolver mounted still blames the depth.explain-read-verdict-inputs.test.ts, through the realSharingService:hierarchy-scope-resolverkernel service, where the sharing plugin looks it up, and hands the sharing service that same lookup. Before, it gave the sharing service a resolver the kernel did not know.record.visibleagree), and the depth cell names the absent resolver.permission.test.ts, 2 rows forfindHierarchyScopeGrants.showcase-scope-depth-fallback.dogfood.test.ts, the open-edition world. It used to publish theunit_and_belowdefault profile with no resolver mounted, which this PR now refuses.INVALID_METADATA, rulehierarchy-scope-capability-required.Red legs (
scripts/ablation-replace.mjs, each leg restored toblob == HEAD):hierarchyResolverMounted()answersundefined, the behaviour onmain)expected { code: undefined, status: undefined } to deeply equal { code: 'INVALID_METADATA', … }, the card's 200false(main's cell)expected 'The record's owner (u_reporter) is o…' to match /^Hierarchy resolver absent …/, the card's misattributionReadings at
1267e3cb:All runs went through the shared verify lock, so the seconds are shared-box seconds.
pnpm build(at4cf2c1a5, source unchanged since): exit 0.@objectstack/metadata-protocol: 267 files, 21549 passed, 19 skipped.@objectstack/plugin-security: 227 files, 4929 passed, 45 skipped.@objectstack/spec: 725 files, 21965 passed, 1 todo.eslint --no-inline-configon the three packages and the dogfood file: exit 0.scripts/pm/dispatch-gates.mjs: 99 gates derived, 99 run, all exit 0, 0 NOT-MEASURED (at4cf2c1a5). The derived list is identical at1267e3cb. The 53 of them that scan sources (all but the self-tests, the spec-package checks and the build-output checks) were re-run there, all exit 0.pnpm --filter @objectstack/dogfood test, at4cf2c1a5): 296 files passed, 3 skipped, 1 failed, the fallback file above. It is fixed in1267e3cband now passes 4/4. The other files share that tree's source.packages/runtimeshare-links-enforcement-context.test.tsandstandalone-stack.test.ts, the other runtime suites that declare hierarchy scopes: 52 passed.Acceptance notes
SaveMetaItemResponseSchema.advisories's.describe()(packages/spec/src/api/protocol.zod.ts) says the findings come from rules "applied to this body on its way toactive". A draft save can now carry this one warning too. The describe is left as is, because this claim allows exactly onepackages/specchange, the helper export. It is description text only, and the key's shape and meaning (non-gating findings, warning or info) hold.Generated by Claude Code