Skip to content

metadata save + explain: an unlicensed hierarchy scope saves silently at runtime though defineStack refuses it, and explain blames the depth #26188

Closed

Description

@objectstack-fleet

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–2075 gains a resolver-absent branch ahead of the depth verdict: "hierarchy resolver absent — requires enterprise; depth evaluated as 'own'".
  • Framework code only. Cloud mounts SecurityEnterprisePlugin by licence (cloud apps/objectos-ee/objectstack.config.ts:1333, :1597) and changes nothing.

Anchors (re-read on origin/main 83a26c38)

  • packages/spec/src/stack.zod.ts:
    • validateHierarchyScopeCapability is at :4817–4849 (doc comment :4817–4824, function :4825–4849). The ruling's :4818–4845 has drifted to this range.
    • The error class carrying STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED is at :3445–3452 and 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 cited sharing-service.ts:377, which is a phantom-anchor notice comment on both cdd4eea9 and 83a26c38. Corrected.
  • Save doors: PUT /api/v1/meta/:type/:name (packages/rest/src/rest-route-ledger.ts:250, client meta.saveItem) and POST /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–2075 holds. 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, unit or unit_and_below is refused, with a diagnostic that names the enterprise requirement (hierarchy-security).
    • A draft save succeeds and its _diagnostics carries the same message as a warning.
  • 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

  1. added
    bugSomething isn't working
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 26, 2026
  2. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8 · card thread
    Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_01M4SCfSer74vqRR1SLn1hWP
    Account: 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:server
    Seat: domain:server#1
    Ruling-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 reuses INVALID_METADATA 422 with rule: 'hierarchy-scope-capability-required', and the draft-save warning rides in the save answer's declared advisories; no ledger row, and ⛔ no _diagnostics added to the save answer. One new @objectstack/spec export of the existing hierarchy-scope helper is allowed (one scope list, one sentence; patch on spec, no schema change). ⛔ Not delegated-admin-gate.ts or error-code-ledger.zod.ts (#26189's). Any other packages/spec/** change: stop and explain in the report
    Container & 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 defineStack refuses it, and explain blames the depth
    Thread-read: none
    Premise: holds (read 2026-10-26T18:21Z at origin/main 21371552b0, which contains #26064: card open, unassigned, pm:queue, no claim)
    Serial constraints cleared: #26064 merged (it is on 21371552b0); no open PR touches explain-engine.ts or the authoring gates; #26189 (in parallel) stays off this surface; the dev re-measures before pr_create

  4. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

    PR #26305 merged at 2026-10-26T21:00Z by the merge queue as b029c9036f on main (Fixes #26188; ACCEPT 6426567469).

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26305-*' 0 refs
    origin/main by content git cat-file -e origin/main:packages/metadata-protocol/src/runtime-authoring-gate.hierarchy-scope.test.ts present
    Ancestry git merge-base --is-ancestor b029c9036f origin/main holds

    State: this card closed by Fixes (completed); pm:dispatched removed.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions