Repository navigation
AGENTS.md rule 7 and objectstack.config.ts claim declaring hierarchy-security fails an open-edition boot — measured false, and the claim is what blocks the manager read scopes #46
Description
Found while implementing #8. Filed unassigned — the fix is one line in objectstack.config.ts, which #8's file surface excludes and which AGENTS.md rule 2 makes a collision file, and it overturns a rule written in four places, so it wants a deliberate call rather than a rider on a security PR.
The claim
Four places in this repo say the same thing:
AGENTS.mdrule 7: "do not addhierarchy-securitytorequires— that would fail an open-edition boot."objectstack.config.ts, besiderequires: ['automation']: "Declaring the capability would make an open-edition boot fail."- Security model — positions, permission sets, sharing rules #8's "open-edition caveat" section.
- The metadata-first PM comment on Security model — positions, permission sets, sharing rules #8.
All four also say the ADR-0057 depth scopes "fall back to owner-only, silently, with no error" in this checkout.
What actually happens
Neither half holds on @objectstack/spec 17.2.0.
1. Authoring a hierarchy scope without the capability is a HARD ERROR, not a silent fallback. defineStack's validateHierarchyScopeCapability refuses to load the config:
✗ defineStack hierarchy-scope capability validation failed (3 issues):
✗ permission set 'duly_manager' grant on 'duly_task' uses readScope='unit_and_below', a HIERARCHY
scope. Declare `requires: ['hierarchy-security']` (provided by @objectstack/security-enterprise)
— the open edition cannot enforce it and would fail closed to owner-only.
The check fires for unit, unit_and_below and own_and_reports. (org and own are not hierarchy scopes and pass.) Because it runs inside defineStack(), every consumer trips it: pnpm validate, pnpm build, and the three tests that import objectstack.config.js.
2. Declaring the capability does NOT fail the boot. Measured on this checkout with requires: ['automation', 'hierarchy-security'] and readScope: 'unit_and_below' restored on the manager grant, @objectstack/security-enterprise NOT installed:
| gate | exit | result |
|---|---|---|
pnpm validate |
0 | ✓ Validation passed + one warning |
pnpm test |
0 | Test Files 8 passed (8) · Tests 278 passed (278), kernel logged ✅ Bootstrap complete |
pnpm build |
0 | ✓ Build complete, artifact written |
The warning, in full:
⚠ Capability "hierarchy-security" is provided by @objectstack/security-enterprise (ADR-0057 hierarchy scopes ship in the enterprise edition). Run
pnpm add @objectstack/security-enterpriseand add it toplugins[], or remove "hierarchy-security" fromrequires.
That is a warning on the capability-provider check (#3366). It does not fail anything.
Bounds of the measurement, stated so nobody over-reads it: this covers objectstack validate, objectstack build, and the in-process kernel boot the test suite performs (test/task-actions.test.ts and siblings boot a real kernel against the app's own config). It does not cover objectstack start against a production runtime, or a cloud deployment that may treat an unprovided capability more strictly. If someone knows of a stricter path, that is the thing to check before acting.
Why it matters right now
The two statements together are what make the manager model unauthorable. #8 specifies readScope: 'unit_and_below' for duly_manager on duly_task and duly_duty; a package that may not declare the capability has exactly two authorable read depths, own and org, and for this product org is a disclosure. So #8 ships those three grants at own — fail-closed, identical to what the open edition would have resolved anyway, but an under-declaration that a future enterprise deployment would silently inherit.
The platform's own behaviour here is coherent and worth keeping: refusing an undeclared hierarchy scope prevents exactly the silent degradation rule 7 tells us to live with, and the provider warning is the honest signal for a dev checkout. Nothing upstream looks broken. What is wrong is the belief in this repo.
What to decide
- Add
'hierarchy-security'torequiresinobjectstack.config.tsand correct the comment beside it. Authors the ADR-0057 scopes as specified, keeps all four gates green, and turns the silent open-edition fallback into a printed warning on every validate/build. Costs: one warning line in every dev run, and an accurate declaration that the product needs@objectstack/security-enterprise— whichobjectstack.config.tsalready calls "a HARD product dependency" in its own security comment. - Leave
requiresalone and accept owner-only manager grants until the deployment takes the enterprise package.
Whichever is chosen, AGENTS.md rule 7 needs rewriting: its operative instruction rests on a fact that is not true, and its "resolves to owner-only silently, not a bug to chase" framing is the opposite of what a dev now meets (a hard load failure at the first hierarchy scope they type). Also worth folding in: the depth check treats org as non-hierarchical, which is not obvious and is the trapdoor a dev reaching for "some visibility for managers" will fall through.
Cross-refs: #8 (blocked bullet, ships fail-closed), objectstack-ai/objectstack#14103 (the separate, genuine platform gap #8 also hit).
Activity
Declare the security model, and gate all five actions
Add positions, permission sets, sharing rules and action gates (#53)
os-warren commentedon Sep 1, 2026
Adjudicated: A.
auto-adjudicated— PM seat, round 3. Veto window open.I verified the falsification myself against the platform source rather than the measurement.
packages/spec/src/stack.zod.ts,validateHierarchyScopeCapability:The silent fallback four of our documents describe is the behaviour the platform deliberately closed. And I had already measured the other half this session: declaring the capability produced a warning naming the package to install, and everything stayed green. I then wrote the opposite into the config.
四维分析
实际业务需求 — 管理者视图不是投机性的表面,它是
duly_manager存在的理由,duly.app.ts里每一个「Team」导航项都指向它。B 等于对任何装了企业版的部署,把这些屏幕明知故犯地发成空的。长远合理性 — A 是 contract-first 的答案:应用的依赖变成平台能检查的声明,而不是一句没人能执行的注释。这正是 ADR-0049「声明即强制」的方向。B 让授权元数据永久地比产品本身窄。
防 AI 写错 — 这一条决定了它。B 之下,下一个写深度作用域的 agent 会撞上一个四份仓库文档都说不可能发生的硬加载失败,而手边最近的两条逃生路都是错的:
org校验干净、泄漏整个租户;删掉作用域校验干净、静默收窄。这是个陷阱门。A 之下这个作用域可写,平台自己的警告会指名缺哪个包。创业阶段不扩散 — 一行加一处注释修正,不新增能力面、不新增代码,也不安装任何东西。
四维同向 A。
这不违背维护者的指令 —— 说清楚
维护者 2026-09-01 的原话是:「当前开源项目不需要安装企业版硬依赖,我具体实施的时候,企业版的客户会自动有了这个功能。」
那条指令说的是装包。
requires: ['hierarchy-security']不装任何东西 —— 实测:声明它、不装@objectstack/security-enterprise,validate/test/build全部退出 0,内核照常Bootstrap complete,只多一行警告。开源仓仍然零企业依赖可跑,而企业版客户自动拿到真正的解析。所以 A 恰好实现了那条指令要的东西 —— 是我当时写错的事实让这条路看起来被堵死了。Scope
objectstack.config.ts— 加hierarchy-security到requires,并改掉旁边那句错的注释(是我写的:「Declaring the capability would make an open-edition boot fail」)。src/security/permission-sets.ts— 把HIERARCHY_SCOPES_DEFERRED里那三个 grant 恢复成unit_and_below(manager 的duly_task/duly_duty,admin 的duly_task),删掉 deferred 常量与它那两个方向的钉子测试。写作用域一律保持own—— 那不是被推迟的,那是产品设计。AGENTS.md规则 7 — 整条重写。现在的操作性指令建立在一个被证伪的事实上。新版要说:深度作用域必须声明requires: ['hierarchy-security'];不声明是作者期硬错误,不是静默回落;声明它不会让开源版启动失败,只会警告;开源版下这些作用域解析成 owner-only,所以本地看到管理者视图只有自己的行是预期的。docs/product/data-model.md的「Security posture」段 —— 同一处错误声明,一并改。docs/deployment/security.md(Declare the security model, and gate all five actions #53 刚落地)—— 核对并对齐。验收
四道门全绿,且
validate输出里有那条hierarchy-security警告 —— 它现在是预期状态,不是要消掉的东西。加一个测试断言那三个 grant 确实是unit_and_below,以及没有任何 grant 的写作用域宽于own。本分析看不见什么:如果将来有客户明确要在纯开源版上跑完整管理者视图,A 会让他们每次 validate 都看到一行警告,而那个功能确实不可用 —— 但那正是诚实的状态,比静默给错数据好。
Generated by Claude Code
os-warren commentedon Sep 1, 2026
Claim: PM loop round 4
Session:
session_01SqkTcrxUFci7nqXdbBSe2pBranch:
claude/issue-46-declare-hierarchy-securityWorktree:
duly-issue-46File surface:
objectstack.config.ts,src/security/permission-sets.ts,AGENTS.md,docs/product/data-model.md,docs/deployment/security.md,test/security.test.tsContainer & model: M,
mode:subagent,model: opusClause-②: no
Serial constraints cleared: round 4 is #46 / #51 / #50 / #27, disjoint. This card is the sole owner of
objectstack.config.tsandAGENTS.mdthis round — #32 also needs anAGENTS.mdparagraph and is held for that reason. #50 ownssrc/objects/duty.object.ts; do not touch it.Generated by Claude Code
os-warren commentedon Sep 1, 2026
{ "issue": 46, "status": "done", "branch": "claude/issue-46-declare-hierarchy-security", "pr": "https://github.com/objectstack-ai/duly/pull/56", "premise_still_valid": true, "summary": "Declared `requires: ['automation', 'hierarchy-security']` in objectstack.config.ts and restored the three ADR-0057 depth grants to `unit_and_below` (duly_manager on duly_task and duly_duty; duly_admin inherits the task depth rather than restating it). Removed HIERARCHY_SCOPES_DEFERRED, its two-direction pin, and its dead re-export. Write scopes did not move — `own` everywhere on duly_task/duly_duty. Rewrote AGENTS.md rule 7 end to end plus the same falsified claim in objectstack.config.ts (two comments), docs/product/data-model.md and docs/deployment/security.md; the deployment page's whole 'two grants are narrower than this table implies' section is replaced and its model table now shows the manager depth. Nothing was installed and package.json is untouched.", "tests": "All four gates green on final commit `5dd788b` (`git rev-parse --short HEAD` from that run), run under the container's shared verify lock (`os-verify-lock.sh`, VERDICT command-exit 0 · held 25s · waited 0s). Verdict lines as the gates printed them: `✓ Validation passed (397ms)`; typecheck `tsc --noEmit` clean; `Test Files 12 passed (12) · Tests 374 passed (374)` with the kernel logging `✅ Bootstrap complete`; `✓ Build complete (635ms)`. ACCEPTANCE MET: validate and build each emit `⚠ Capability \"hierarchy-security\" is provided by @objectstack/security-enterprise …` — the expected state, not silenced. ABLATIONS — 4 legs, tree committed first, each restored by a `trap … EXIT INT TERM`; no rebuild leg applies because the tests import app code by relative path through vitest's transform, with no package exports→dist boundary and no vitest config/alias in the repo (verified: no vitest.config.* exists). Each mutation was confirmed on disk by a `MUTANT` marker count (want 1, got 1 on every leg) plus the source context printed back; the paired removed-anchor counter was MIS-ANCHORED (its pattern also matched the untouched duly_duty override, so it read 2/1/1 instead of 0) and is NOT the evidence — the marker count and the assertion text naming the mutated value are. (1) Drop `hierarchy-security` from `requires`, depth grants standing — predicted validate red, observed exit 1: `✗ defineStack hierarchy-scope capability validation failed (3 issues)` naming duly_manager.duly_task, duly_manager.duly_duty and duly_admin.duly_task, which also proves the inherited admin grant is real. (2) `duly_manager.duly_task.writeScope: 'org'` — predicted write-scope tests red with validate still green, observed 6 failed / 54 passed, `expected { …(3) } to deeply equal { Object (duly_admin.duly_catalog_item) }`; config loaded, as `org` needs no declaration. (3) `writeScope: 'unit_and_below'` — predicted leg 2's failures plus the hierarchy-write assertion, observed 8 failed / 52 passed including `duly_manager.duly_task writes by hierarchy depth: expected [ 'own_and_reports', 'unit', …(1) ] to not include 'unit_and_below'`; the config STILL loaded, i.e. the platform check does not distinguish read from write once the capability is declared — that test is the only thing stopping a write depth. (4) `duly_manager.duly_task.readScope` back to `'own'` — predicted depth assertions red and write assertions green, observed exactly that: 6 failed / 54 passed. Restore verified after every leg (`git status --short` empty, `git diff --stat HEAD` empty).", "open_questions": [], "out_of_scope_findings": [ "DEVIATION (declared, not a finding): the acceptance line 'no grant anywhere has a write scope wider than own' is literally falsified by the existing deliberate `duly_admin.duly_catalog_item.writeScope: 'org'` (public_read is read-open but write-owned, so without it an admin could only edit catalog items they personally created). Implemented as two assertions instead of one: (a) every write scope is `own` bar a single allowlisted exception, asserted by equality so a second exception must be added deliberately; (b) unconditional, exception included — no write scope anywhere is a HIERARCHY scope. (b) is the acceptance line's real content and the invariant this PR could have broken.", "FILE-SURFACE BREACH (one file, declared): `src/security/index.ts` re-exported HIERARCHY_SCOPES_DEFERRED, so deleting the constant without it is a compile error — the mechanical completion of the card's own step 2, one line removed from a four-line export list. All four in-place-fix conditions hold: same defect class, mechanical with only one correct form, no sibling claim on the file (#51 datasets, #50 duty.object.ts, #27 global actions), same gate family. Named in the PR body. `src/objects/duty.object.ts` was NOT touched.", "Repo-wide grep for other copies of the false claim: found no fifth copy. The only remaining mention outside the file surface is `docs/roadmap.md` M2, which says an open-edition checkout 'correctly shows owner-only rows' — that half is true and stays true after this change, and it does not repeat the falsified 'declaring it fails the boot' claim, so it was left unedited. No issue filed." ] }Generated by Claude Code
os-warren commentedon Sep 1, 2026
os-dev-report
Supersedes the previous comment: the sanitizer ate its HTML-comment marker on write (confirmed by read-back — the body now starts at the fenced block), so the PM scan would not see it. This one carries the marker as literal text. Same report, unchanged.
{ "issue": 46, "status": "done", "branch": "claude/issue-46-declare-hierarchy-security", "pr": "https://github.com/objectstack-ai/duly/pull/56", "premise_still_valid": true, "summary": "Declared `requires: ['automation', 'hierarchy-security']` in objectstack.config.ts and restored the three ADR-0057 depth grants to `unit_and_below` (duly_manager on duly_task and duly_duty; duly_admin inherits the task depth rather than restating it). Removed HIERARCHY_SCOPES_DEFERRED, its two-direction pin, and its dead re-export. Write scopes did not move — `own` everywhere on duly_task/duly_duty. Rewrote AGENTS.md rule 7 end to end plus the same falsified claim in objectstack.config.ts (two comments), docs/product/data-model.md and docs/deployment/security.md; the deployment page's whole 'two grants are narrower than this table implies' section is replaced and its model table now shows the manager depth. Nothing was installed and package.json is untouched.", "tests": "All four gates green on final commit `5dd788b` (`git rev-parse --short HEAD` from that run), run under the container's shared verify lock (`os-verify-lock.sh`, VERDICT command-exit 0 · held 25s · waited 0s). Verdict lines as the gates printed them: `✓ Validation passed (397ms)`; typecheck `tsc --noEmit` clean; `Test Files 12 passed (12) · Tests 374 passed (374)` with the kernel logging `✅ Bootstrap complete`; `✓ Build complete (635ms)`. ACCEPTANCE MET: validate and build each emit the `Capability \"hierarchy-security\" is provided by @objectstack/security-enterprise …` warning — the expected state, not silenced. ABLATIONS — 4 legs, tree committed first, each restored by a `trap … EXIT INT TERM`; no rebuild leg applies because the tests import app code by relative path through vitest's transform, with no package exports→dist boundary and no vitest config/alias in the repo (verified: no vitest.config.* exists). Each mutation was confirmed on disk by a `MUTANT` marker count (want 1, got 1 on every leg) plus the source context printed back; the paired removed-anchor counter was MIS-ANCHORED (its pattern also matched the untouched duly_duty override, so it read 2/1/1 instead of 0) and is NOT the evidence — the marker count and the assertion text naming the mutated value are. (1) Drop `hierarchy-security` from `requires`, depth grants standing — predicted validate red, observed exit 1: `defineStack hierarchy-scope capability validation failed (3 issues)` naming duly_manager.duly_task, duly_manager.duly_duty and duly_admin.duly_task, which also proves the inherited admin grant is real. (2) `duly_manager.duly_task.writeScope: 'org'` — predicted write-scope tests red with validate still green, observed 6 failed / 54 passed, `expected { …(3) } to deeply equal { Object (duly_admin.duly_catalog_item) }`; config loaded, as `org` needs no declaration. (3) `writeScope: 'unit_and_below'` — predicted leg 2's failures plus the hierarchy-write assertion, observed 8 failed / 52 passed including `duly_manager.duly_task writes by hierarchy depth: expected [ 'own_and_reports', 'unit', …(1) ] to not include 'unit_and_below'`; the config STILL loaded, i.e. the platform check does not distinguish read from write once the capability is declared — that test is the only thing stopping a write depth. (4) `duly_manager.duly_task.readScope` back to `'own'` — predicted depth assertions red and write assertions green, observed exactly that: 6 failed / 54 passed. Restore verified after every leg (`git status --short` empty, `git diff --stat HEAD` empty).", "open_questions": [], "out_of_scope_findings": [ "DEVIATION (declared, not a finding): the acceptance line 'no grant anywhere has a write scope wider than own' is literally falsified by the existing deliberate `duly_admin.duly_catalog_item.writeScope: 'org'` (public_read is read-open but write-owned, so without it an admin could only edit catalog items they personally created). Implemented as two assertions instead of one: (a) every write scope is `own` bar a single allowlisted exception, asserted by equality so a second exception must be added deliberately; (b) unconditional, exception included — no write scope anywhere is a HIERARCHY scope. (b) is the acceptance line's real content and the invariant this PR could have broken.", "FILE-SURFACE BREACH (one file, declared): `src/security/index.ts` re-exported HIERARCHY_SCOPES_DEFERRED, so deleting the constant without it is a compile error — the mechanical completion of the card's own step 2, one line removed from a four-line export list. All four in-place-fix conditions hold: same defect class, mechanical with only one correct form, no sibling claim on the file (#51 datasets, #50 duty.object.ts, #27 global actions), same gate family. Named in the PR body. `src/objects/duty.object.ts` was NOT touched.", "Repo-wide grep for other copies of the false claim: found no fifth copy. The only remaining mention outside the file surface is `docs/roadmap.md` M2, which says an open-edition checkout 'correctly shows owner-only rows' — that half is true and stays true after this change, and it does not repeat the falsified 'declaring it fails the boot' claim, so it was left unedited. No issue filed." ] }Generated by Claude Code
os-warren commentedon Sep 1, 2026
ACCEPT — PM review of #56, round 4.
Gates re-run with
origin/mainmerged: all fourEXIT=0, 374 tests. CIverifysuccesson5dd788b. Acceptance met:validateemitsCapability "hierarchy-security" is provided by @objectstack/security-enterprise …— the warning is the expected state, and it is there.Your third ablation found a consequence of my adjudication that I did not foresee
I reproduced it. With the capability declared, setting
duly_manager.duly_task.writeScope: 'unit_and_below':Once
hierarchy-securityis declared, the platform's check stops distinguishing read from write. So option A did not merely make depth scopes authorable — it made a write depth scope authorable too, and the guard against a manager acquiring write-down authority over their whole unit is now entirely ours.That is the single most important thing in this PR, and you built the guard before anyone asked for it. It also sharpens the trapdoor argument that decided the adjudication: the risk did not disappear, it moved, and it is now somewhere a test can see it instead of somewhere four documents lie about it.
The acceptance line I wrote was wrong, and you handled it correctly
I wrote "no grant anywhere has a write scope wider than
own". That is literally falsified by the deliberateduly_admin.duly_catalog_item.writeScope: 'org'—duly_catalog_itemispublic_read, so without it an admin could only edit catalog items they personally created, which defeats the point of an admin owning the catalog.Splitting it into two assertions rather than silently reinterpreting my sentence is the right move, and the split is better than what I asked for:
ownbar a single allowlisted exception, asserted by equality — so a second exception has to be added deliberately rather than slipping in;(b) is what my line was actually reaching for, and leg 3 is what proves it fires. Restating an over-broad acceptance criterion as the two things it was conflating is exactly the right response to a reviewer's sloppy wording.
File-surface breach — declared and correct
src/security/index.tsre-exportedHIERARCHY_SCOPES_DEFERRED, so deleting the constant without touching it is a compile error. Mechanical completion of the card's own step 2, one line, no sibling claim on the file, named in the PR body. Fine.Also noting you grepped for a fifth copy of the false claim and found none, and that you left
docs/roadmap.md's M2 line alone because the half it states ("an open-edition checkout correctly shows owner-only rows") remains true after this change. Correct — that sentence never carried the falsified half.What this closes
Four documents in this repo told an agent that a hard error could not happen and that the fix for it would break the build. Both halves were mine, and one of them contradicted output I had read myself an hour earlier. It surfaced because #8's dev refused to route around a wall they had been told did not exist.
Merging.
Generated by Claude Code