Repository navigation
metadata save + explain: an unlicensed hierarchy scope saves silently at runtime though defineStack refuses it, and explain blames the depth #26188
Description
Filing gate ②: a maintainer ruling, carried out as child 2 of the nine that #26059's ruling (Q3) names. ⛔ Not a claim.
When no hierarchy resolver is mounted (community mode, no hierarchySecurity licence), a runtime save of a hierarchy scope is accepted without a word. PUT of a permission set with readScope: 'unit_and_below' answers 200 with _diagnostics.valid: true and logs nothing, and the scope then falls closed to owner-only. defineStack refuses the same declaration at compile time. On top of that, explain tells a manager that a subordinate's record is "outside the caller's 'unit_and_below' read depth" when the subordinate is in their unit. The real cause is that the resolver is absent. Neither path lets anyone see more than they should (both fail closed), but both are silent or wrong about why.
Part of #26059
Ruling-ref: 6416233738 (#26059 (comment))
Ruling: batch #396 item 1, letter Q1 是 / Q2 不做 / Q3 分拆 / Q4 框架. This card is Q3 child 2 = Q4 (框架): items 9a + 9b in one dispatch (server, p2). Brief 6415795834, item-by-item reading 6415971852 (items 9a, 9b).
Filed by domain:ui seat 2 (domain:ui#2) · os-steve · session session_01SMps7wq7rBTk53e8KTSEup, first claimant of #26059. Domain, grade and route are triage's on first touch.
Business reason. An admin on a community runtime, self-hosted or cloud community mode, who saves "my unit and below" is told the save is valid. Users then see only their own records, and explain points at the wrong cause. The metadata says one thing and the runtime does another: the inert declaration ADR-0078 forbids and ADR-0049 calls worse than absent.
Reader who acts. The domain:server execution seat, dispatch step: one os-dev, one PR in framework code (the runtime metadata save door, and packages/plugins/plugin-security/src/explain-engine.ts). Cloud changes nothing.
Duplicate check. A local scan of the 1,962 newest issues (all states, #22168–#26181) for validateHierarchyScopeCapability|STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED|hierarchy.{0,40}(resolver absent|licen|enterprise).{0,200}(save|PUT|explain|diagnos) found 1 hit, not this (#24995, webhook requires). #16870 (closed) is the adjacent VA-beside-depth diagnostic, not this.
Ruled scope (ruling Q3 item 2 / Q4)
- 9a. The runtime metadata save door asks the kernel whether the hierarchy resolver is mounted. When it is not, the door answers with a diagnostic that names the enterprise requirement.
- Without the resolver the open sharing service fails closed.
- The compile path already refuses:
validateHierarchyScopeCapability/STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED. This card makes the runtime path match it (ADR-0078 / ADR-0049). - Shape, unless the maintainer says otherwise: refuse on publish, warn on a draft save.
- 9b.
explain-engine.ts:2040–2075gains a resolver-absent branch ahead of the depth verdict: "hierarchy resolver absent — requires enterprise; depth evaluated as 'own'". - Framework code only. Cloud mounts
SecurityEnterprisePluginby licence (cloudapps/objectos-ee/objectstack.config.ts:1333,:1597) and changes nothing.
Anchors (re-read on origin/main 83a26c38)
packages/spec/src/stack.zod.ts:validateHierarchyScopeCapabilityis at:4817–4849(doc comment:4817–4824, function:4825–4849). The ruling's:4818–4845has drifted to this range.- The error class carrying
STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIREDis at:3445–3452and is thrown at:5459–5464. - The code is registered at
packages/spec/src/api/error-code-ledger.zod.ts:1573.
- Capability token
hierarchy-security:packages/spec/src/kernel/platform-capabilities.ts:76,:221–225. - Fail-closed without a resolver:
packages/plugins/plugin-sharing/src/sharing-service.ts:2084–2085(resolveOwnerScopeIds:const resolver = this.hierarchyResolver?.(); if (!resolver) return [me];). The late-bound lookup is documented at:468–471("Returns null in the open edition"). The ruling citedsharing-service.ts:377, which is a phantom-anchor notice comment on bothcdd4eea9and83a26c38. Corrected. - Save doors:
PUT /api/v1/meta/:type/:name(packages/rest/src/rest-route-ledger.ts:250, clientmeta.saveItem) andPOST /api/v1/meta/:type/:name/publish(:268). The dev names the handler that holds the check. packages/plugins/plugin-security/src/explain-engine.ts:2040–2075holds. The misleading arm is:2061–2064(sharingMatches === false, text at:2063); the new branch goes ahead of it.- Cloud
apps/objectos-ee/objectstack.config.ts:1333,:1597: not re-read. This seat has no access to objectstack-ai/cloud, so the line numbers are the ruling's.
Expected / acceptance
- On a runtime without the hierarchy resolver:
- Publishing a permission set whose grant uses
own_and_reports,unitorunit_and_belowis refused, with a diagnostic that names the enterprise requirement (hierarchy-security). - A draft save succeeds and its
_diagnosticscarries the same message as a warning.
- Publishing a permission set whose grant uses
- With the resolver mounted, nothing changes.
- The refusal's code sits in the ADR-0112 ledger, either reused (
STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED) or a new row; the PR names which. - explain on a resolver-absent deployment reports "hierarchy resolver absent — requires enterprise; depth evaluated as 'own'" in the depth layer, not "outside the caller's … read depth". One pinned explain cell.
- Tests pin both the licensed and the resolver-absent posture for 9a and 9b.
Activity
objectstack-fleet commentedon Oct 26, 2026
Triage: first grade — as filed ·
pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-26T15:59Z⛔ Not a claim, ⛔ not a dispatch.
Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Claim: PM loop round 8 · card thread
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01M4SCfSer74vqRR1SLn1hWPAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-qp1ljk(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: 6416233738 (#26059, maintainer ruling Q1 是 / Q2 不做 / Q3 分拆 / Q4 框架; this card is Q3 child 2 = Q4, items 9a and 9b) and triage first grade (2026-10-26T15:59Z). Without the hierarchy resolver, publishing a hierarchy-scope grant is refused and a draft save warns. explain names the absent resolver ahead of the depth verdict. Framework only; cloud is unchanged.
File surface: the runtime authoring gate (
protocol-authoring-gate.ts/runtime-authoring-gate.ts), as a gate-local rule with resolver presence passed in as a host fact, and the call sites that hand it in, so the save door, the per-item publish and the package batch publish read one rule;packages/plugins/plugin-security/src/explain-engine.ts(the resolver-absent branch); their tests. The refusal reusesINVALID_METADATA422 withrule: 'hierarchy-scope-capability-required', and the draft-save warning rides in the save answer's declaredadvisories; no ledger row, and ⛔ no_diagnosticsadded to the save answer. One new@objectstack/specexport of the existing hierarchy-scope helper is allowed (one scope list, one sentence;patchon spec, no schema change). ⛔ Notdelegated-admin-gate.tsorerror-code-ledger.zod.ts(#26189's). Any otherpackages/spec/**change: stop and explain in the reportContainer & model:
S,mode:thread, default model and effort (set at thread open)Clause-②: no (narrowing) — provisional: a publish that used to pass is refused; the dev measures it and declares the ADR-0087 arm
Metadata-protocol: none
Responsibility: an unlicensed hierarchy scope saves silently at runtime though
defineStackrefuses it, and explain blames the depthThread-read: none
Premise: holds (read 2026-10-26T18:21Z at
origin/main21371552b0, which contains #26064: card open, unassigned,pm:queue, no claim)Serial constraints cleared: #26064 merged (it is on
21371552b0); no open PR touchesexplain-engine.tsor the authoring gates; #26189 (in parallel) stays off this surface; the dev re-measures beforepr_createchore(#26188): changeset
test(#26188): type the gate test's issue filter
test(dogfood): the open-edition fallback world now meets the publish …
objectstack-fleet commentedon Oct 26, 2026
os-dev-report
{ "issue": 26188, "status": "done", "branch": "claude/project-thread-qp1ljk", "pr": "https://github.com/objectstack-ai/objectstack/pull/26305", "session": "session_01M4SCfSer74vqRR1SLn1hWP", "premise_still_valid": true, "summary": "9a: a gate-local rule `hierarchy-scope-capability-required` in the runtime authoring gate, with resolver presence (kernel service `hierarchy-scope-resolver`) passed in as a host fact by protocol.ts. With no resolver, publishing a permission set whose grant uses own_and_reports / unit / unit_and_below answers 422 INVALID_METADATA (direct active save, per-item publish, batch publish, restore). A draft save answers 200 with the same entry at severity warning in `advisories`. With a resolver mounted, or on a host with no service registry, nothing changes. 9b: explain-engine's depth record cell gains a resolver-absent branch ahead of the `sharingMatches === false` arm: 'Hierarchy resolver absent — requires enterprise; depth evaluated as 'own'.' New spec export `findHierarchyScopeGrants` (patch, no schema change) is shared with defineStack's validateHierarchyScopeCapability, whose output is byte-identical. INVALID_METADATA was reused, not STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED, per the seat ruling. The PR body says why. No ledger row, and delegated-admin-gate.ts / error-code-ledger.zod.ts are untouched.", "tests": "Head 1267e3cb (main merged at 617f27eb). metadata-protocol: 267 files, 21549 passed, 19 skipped. plugin-security: 227 files, 4929 passed, 45 skipped. spec: 725 files, 21965 passed, 1 todo. New: runtime-authoring-gate.hierarchy-scope.test.ts (7), 3 door rows in protocol.runtime-authoring-gate.test.ts, 2 explain-engine cells, 1 real-SharingService cell in explain-read-verdict-inputs.test.ts, 2 permission.test.ts rows, and the reworked open-edition world in showcase-scope-depth-fallback.dogfood.test.ts (4/4). runtime share-links-enforcement-context + standalone-stack: 52 passed. Ablation via scripts/ablation-replace.mjs, each leg restored to blob == HEAD. A: the door stops reading presence gives 2 door rows red (`expected { code: undefined, status: undefined } to deeply equal { code: 'INVALID_METADATA', … }`). B: the explain branch is false gives 2 red, including the real-stack cell (`expected 'The record's owner (u_reporter) is o…' to match /^Hierarchy resolver absent …/`). C: a draft gets no advisories gives 1 red.", "gates": "Full pnpm build exit 0. Typecheck of the 3 packages exit 0. eslint --no-inline-config on the 3 packages + the dogfood file exit 0. dispatch-gates --commands: 99 derived. All 99 run at 4cf2c1a5, all exit 0. --ran: '99 derived famil(ies) accounted for — 99 run, 0 NOT-MEASURED'. The derived list is identical at 1267e3cb, and its 53 source-scanning gates were re-run there, all exit 0 (the self-tests, spec-package checks and build-output checks were not re-run: the source tree is unchanged since 4cf2c1a5, and only one dogfood test file was added). CI check-runs on #26305: not yet read at hand-back.", "dogfood_battery": "4cf2c1a5 · os-verify-lock: VERDICT command-exit 1 · held the lock 1162s (shared-box seconds) · Test Files 1 failed | 296 passed | 3 skipped (300) · Tests 2526 passed | 19 skipped (2545). The failure was a suite failure in beforeAll: showcase-scope-depth-fallback.dogfood.test.ts 'open edition — same fallback profile fails CLOSED without the enterprise resolver'. Its fixture PUT the unit_and_below profile with no resolver mounted, which this PR now refuses (422). Fixed in 1267e3cb (a test-only commit): the world pins the 422 + rule first, then saves with the reference resolver mounted and unmounts it, so the fail-closed read is still proven. File re-run at 1267e3cb: VERDICT command-exit 0, Test Files 1/1, Tests 4/4. The full battery was not re-run at 1267e3cb.", "mcp_calls": "5 — all reads: mcp__github__issue_read x4, mcp__github__pull_request_read x1. No MCP writes.", "api_writes": "2 relay writes: pr_create #26305 (fleet-write run 40391561955, read-back identical, assignee os-project-manager) and this os-dev-report comment via post-stamped. git pushes to claude/project-thread-qp1ljk are not REST writes. Write budget spent: push + pr_create + report = 3 of 3.", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed — SaveMetaItemResponseSchema.advisories .describe() (packages/spec/src/api/protocol.zod.ts) says the findings come from rules applied 'on its way to active'. A draft save can now carry this one warning, so the text is partly stale. It is description only, and the claim allowed one packages/spec change. It is recorded in the PR's Acceptance notes.", "deviation: Clause-② is declared `yes (narrowing: …; widened: findHierarchyScopeGrants export, ExplainEngineDeps.hierarchyResolverMounted)`, not the provisional `no (narrowing)`, because the spec export and the optional deps member widen a public face.", "deviation: two test harnesses changed beyond the new cells. explain-read-verdict-inputs.test.ts now registers its stub resolver as the kernel service (it used to hand SharingService a resolver the kernel did not know). showcase-scope-depth-fallback.dogfood.test.ts is described under dogfood_battery." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Landed
PR #26305 merged at 2026-10-26T21:00Z by the merge queue as
b029c9036fonmain(Fixes #26188; ACCEPT6426567469).git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26305-*'origin/mainby contentgit cat-file -e origin/main:packages/metadata-protocol/src/runtime-authoring-gate.hierarchy-scope.test.tsgit merge-base --is-ancestor b029c9036f origin/mainState: this card closed by
Fixes(completed);pm:dispatchedremoved.Generated by Claude Code