Uh oh!
There was an error while loading. Please reload this page.
Declare the security model, and gate all five actions - #53
Merged
Conversation
Three flat positions, three composed permission sets, no sharing rules — and `requiredPermissions` on every action, which closes#30 and #40. The work log is closed to everyone but its owner, administrators included, and no set carries a write scope wider than `own` on `duly_task` / `duly_duty`. Both are asserted across all three sets rather than left to review. Two things the card asked for are not shippable on protocol 17.2.0 and are documented rather than approximated: a sharing rule cannot name the record owner's manager (objectstack#14103), and a hierarchy read depth cannot be authored without a capability this package may not declare (#46). Both fail closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
os-warren
marked this pull request as ready for review
September 1, 2026 05:46
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Sep 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #8
Fixes#30
Fixes#40
Three flat positions, three composed permission sets, capability gates on all five actions, and a deployment page that says what a rollout still has to do by hand.
The first line is
Part of, not a closing keyword, and that is deliberate: two things the card specifies are not shippable on protocol 17.2.0 (below), and one of them needs a maintainer ruling recorded on #8 before that card is closed. #30 and #40 are complete and do close here.Gates, all four green at
8d5cc29— the tip of this branch, re-run on the restored tree after the ablations below:pnpm validate✓ Validation passed (330ms)·Security: 3 Positions 3 Permissionspnpm typechecktsc --noEmit, exit 0, no outputpnpm testTest Files 9 passed (9)·Tests 334 passed (334)pnpm build✓ Build complete (603ms)The model
duly_memberduly_managerduly_adminduly_taskduly_dutyduly_log_entryduly_catalog_itemduly_assignmentduly.task.update_statusduly.catalog.apply,duly.catalog.sync"Inherited" is literal, not descriptive:
MANAGER_OBJECTSspreadsMEMBER_OBJECTS,ADMIN_OBJECTSspreadsMANAGER_OBJECTS, and the test asserts every non-overridden entry is the same object. A grant is written once, so nobody can widen the work log for managers by editing a copy — there is no copy.Each set is self-contained, so binding one set to one position is a correct deployment.
The two invariants, enforced rather than intended
The work log is closed to everyone but its owner, administrators included. No depth scope, no
viewAllRecords, no sharing rule, walked across all three sets. No set carries a write scope wider thanownonduly_task/duly_duty— every widening in this PR is on the read axis, and the test also proves that the only write bit the manager set adds anywhere isduly_assignment.duly_admin.duly_dutyis the one entry worth reading twice: it getsallowEditand keepswriteScope: 'own'. That is the card's acceptance line beating its permission-set line, and the bit is not inert — an administrator can correct their own duties, which a member cannot. The org-wide correction path is deliberately elsewhere:duly_catalog_sync, which is bounded to cadence fields, capability-gated, and reportable. A correction typed into someone else's duty record is none of those.Deliberate non-obvious calls
duly_catalog_itemgetswriteScope: 'org'on the admin set, and noreadScopeanywhere.public_readis read-open but write-owned —buildWriteFilterapplies toprivateandpublic_readalike — so without the write depth an administrator could only edit catalog items they personally created. ConverselybuildReadFilterreturnsnullbefore depth is consulted for any non-private object, so areadScopethere would be inert and is not authored.duly.catalog.applyandduly.catalog.syncare two capabilities, both granted toduly_admin, answering the question Gate the catalog actions withrequiredPermissions—duly_catalog_applyis currently ungated #30 raises. Nothing is harder to deploy, but a customer who wants an onboarding administrator who cannot rewrite the org's cadence can now express it.isDefault, nofieldsblock, noadminScope, no'*'wildcard.isDefaultis not available: the ADR-0090 D5/D9 anchor tier refuses any set carryingsystemPermissionsor a delete bit, andduly_membercarries both. FLS would only restatereadonlyflags the objects already enforce.requiredPermissions-only file surface. The link is held by two independent checks instead: thecapability-reference-unknownauthor-time rule, and a test asserting every required capability is granted by a set in this package.⛔ Two things the card asks for that this platform version cannot express
Both fail closed. Neither is approximated.
1. No sharing rule can name the record owner's manager — objectstack#14103
ShareRecipientTypeis five static principals, andplugin-sharing'sexpandRecipientresolvesrule.recipient_idonce per rule, never per matched record. The predicate half (record.visibility == "manager") lowers fine; the recipient half has nothing to resolve through.The nearest expressible recipient,
position: 'duly_manager', would hand every marked entry to every manager in the tenant — the disclosure the invariant exists to prevent — so it is not authored as a stopgap.RLS is not a way round it either, despite
sharing-rule-runtime-variable-condition's hint recommending exactly that. On aprivateobject the layers are AND-composed, not OR-composed:and
buildReadFilterreturnsownerMatch(OR'd only with the caller's ownsys_record_sharegrants). An RLS policy can therefore only narrow a private object's readable set. Widening has two doors — the ADR-0057 depth scopes and asys_record_sharerow — and both are shut for a record-relative recipient.duly_assignmenthits the same wall: "readable by the people it is addressed to" is the same shape. Assignees still see their own fanned-outduly_task, which is the row they work.src/security/sharing-rules.tsis an empty array with the whole measurement on it. The file exists precisely so the next author does not rediscover the wall and author their way past it with the recipient that lints clean.2. A hierarchy read depth cannot be authored here — tracked as #46
duly_manageronduly_task/duly_dutyandduly_adminonduly_taskshould readunit_and_below. They ship atown.defineStack'svalidateHierarchyScopeCapabilityis a hard error onunit/unit_and_below/own_and_reportsunless the stack declaresrequires: ['hierarchy-security']. It runs insidedefineStack(), so it takesvalidate,buildand the three tests that import the config. This is not the "silent fallback to owner-only" AGENTS.md rule 7 describes — nothing silent happens; the config will not load.The premise blocking the prescribed fix is measurably false. Rule 7,
objectstack.config.ts, the card and its PM comment all state that declaringhierarchy-security"would fail an open-edition boot". Measured on this checkout with the capability declared,unit_and_belowrestored, and@objectstack/security-enterprisenot installed:pnpm validate✓ Validation passed+ one provider warningpnpm testTests 278 passed (278), kernel logged✅ Bootstrap completepnpm build✓ Build completeSo the one-line fix appears to work. It is not taken here:
objectstack.config.tsis outside this card's file surface, it is the collision file AGENTS.md rule 2 reserves, and overturning a rule written in four places belongs on its own change. #46 carries the full measurement and its bounds.ownis also exactly what an open-edition runtime would have resolvedunit_and_belowto, so nothing about today's behaviour differs — only the honesty of the declaration. The three grants are recorded inHIERARCHY_SCOPES_DEFERREDand pinned in both directions: widen a grant without deleting its row and the test fails; delete a row without widening the grant and the test fails.Ablations — the new tests do fail when they should
Both run on the committed tree with a restoring
trap, mutation confirmed on disk before the run and absence confirmed after. Predicted direction for both was red, and both went red in exactly the named places.Widen
duly_log_entrytoreadScope: 'org'on the manager set (orgchosen overunit_and_belowon purpose — it is authorable, so the only red can come from the guard rather than fromdefineStackrefusing to load):Delete
requiredPermissionsfromduly_catalog_apply:One honest note on the second: the shell marker I printed for it was mis-anchored — it grepped the bare capability string, which also appears three times in prose, so it read
3where I had written "want 0". The disk-landing proof there is the Pythonassert count == 1on the exactrequiredPermissions:line before the replace, plus the targeted red. The first ablation's markers were clean (1 → 0).The
own_and_reports/unit_and_belowhalf of the hierarchy finding was measured the same way, againstobjectstack.config.tsunder a trap; that file is untouched in this diff (git statusclean, verified after each leg).Also filed
objectstack.config.tsclaim declaringhierarchy-securityfails an open-edition boot — measured false, and the claim is what blocks the manager read scopes #46 — the falsifiedhierarchy-securitypremise in AGENTS.md rule 7 andobjectstack.config.ts. Unassigned. Notedocs/product/data-model.md's "Security posture" section carries the same claim and is outside this card's file surface, so it is left for that issue.source: 'catalog', so a self-declared duty is born scoreable #50 —duly_duty.sourcedefaults tocatalog, so a hand-created self-declared duty is born scoreable. Found because the member set grants "create own duty,source: 'self'". Unassigned.Files
src/security/positions.ts,src/security/permission-sets.ts,src/security/sharing-rules.ts(new) ·src/security/index.ts(barrel) ·src/actions/catalog.actions.ts,src/actions/task.actions.ts(requiredPermissionskeys only — no imports added, no handler bodies, noregister-handlers.ts) ·docs/deployment/security.md(new) ·test/security.test.ts(new).objectstack.config.tsuntouched.Generated by Claude Code