Repository navigation
An app-declared permission baseline REPLACES the platform member_default instead of composing with it — every built-in Account destination 403s for members of any such app #7555
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 11, 2026 Claim: PM loop round 1 (identity seat, seat post #6022)
Session:session_01BVc1ekPpi6yaWywAUhfzfd
Branch:claude/issue-7555-baseline-compose
Worktree:objectstack-issue-7555(dev-owned per-task worktree)
Domain:domain:identity
File surface:packages/plugins/plugin-security/src/security-plugin.ts(regions:resolvePermissionSetsForContext, the constructor'sfallbackPermissionSetderivation at ~:520, and the ADR-0090 D5 everyone-anchor boot binding at ~:2280) ·packages/plugins/plugin-security/src/app-default-permission-set.ts· their tests. Stop on breach; explain in the report.
Container & model: M,mode:cloud(dedicated session per the 2026-08-10 M+ ruling),model: claude-opus-5
Serial constraints cleared: #7344 is in flight (cross-lane claim by the spec-surface seat, 2026-08-11T05:11Z) with declared surfacepackages/plugins/plugin-security/src/objects/default-permission-sets.ts+ tests — same package, disjoint files/regions (recorded in the seat post's hot-file serial queue). The dispatch instructs a merge oforigin/mainbefore the PR opens and again if #7344 lands mid-flight. No other open claim touches these files per this round's read.Dispatch rationale (restore-invariant, not escalated — veto window open). ADR-0090 D5 (Accepted) rules the baseline additive: "The fallback cliff is abolished …
everyoneis additive like any other position: baseline ∪ explicit, always", and narrowsisDefaultto "a package-authored suggestion consumed once at install time (D5), never a runtime fallback". The current interim wiring (app-default-permission-set.ts, self-described "interim wiring") makesfallbackPermissionSeta single name, so an app'sisDefaultset displacesmember_default— reproducing the exact cliff shape D5 abolishes (declaring an app baseline silently costs members the entire platform baseline). The 2026-08-11 maintainer ruling on #7344 points the same way ("a platform app's platform object belongs to the platform baseline"). Composing instead of replacing restores the stated invariant; it does not widenmember_defaultitself, so #5491's narrowing is untouched. The maintainer can veto here or on the PR.Out of scope for this dispatch: the nav-prune half (
requiresObjectgives the nav server no prune signal) — that is the #7544 sibling family,domain:metadata.
Generated by Claude Code
Claim amendment (same session
session_01BVc1ekPpi6yaWywAUhfzfd): Container falls back tomode:subagent— the Claude_Code_Remote MCP tools (create_session) are absent from this PM session after repeated tool-registry searches, and the SKILL's dispatch-backend clause prescribes exactly this fallback when they are unavailable ("if absent, say so and fall back tomode:subagent"). Model tier unchanged:opus. Everything else in the claim stands — branch, worktree, file surface, serial constraints.
Generated by Claude Code
Review: ACCEPT — PR #7605 (draft, CI converging; the ready-flip and enqueue follow the gate jobs' real conclusions).
What shipped, verified against the PR's files (not its summary):
- Mechanism confirmed at the site the card presumed:
SecurityPlugin.fallbackPermissionSetwas a single name consumed at 5 sites (additive step, post-resolution fallback, ADR-0106 D7 resolution, D5 everyone-anchor boot binding, explain attribution). The fix makes the human baseline a composed list — app-declared set ∪member_default— via a pure helper (composeHumanBaselinePermissionSets), applied at all 5 sites.nullstill disables the baseline entirely; the agent path (ADR-0090 D10) branches before the composed list is consulted and keeps its restricted ceiling — both pinned by dedicated cases. - Hard edges held:
member_default's own grant rows untouched (The Account app's Notifications entry 403s for every non-admin — no permission set grants sys_inbox_message #7344's in-flight territory, files disjoint — confirmed in the diff);member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491's explicit-allow narrowing undisturbed; D5/D9 high-privilege anchor gate untouched. - Evidence quality: 11 new cases reverse-verified by ablation (reverting the helper turns exactly the 3 composition-bearing cases red — the partition is the point); both dogfood displacement pins flipped to assert the composed substance (not deleted); end-to-end proof on a booted showcase (fresh member under
showcase_member_defaultreadssys_user_preference→ 200, granted by the platform baseline and nothing else). Consumer surfaces enumerated and updated (/auth/me/permissions,/me/appsvia newsecurity.baselinePermissionSetsservice;security.fallbackPermissionSetkeeps its contract). - The Account app's Notifications entry 403s for every non-admin — no permission set grants sys_inbox_message #7344 interplay answered with measurement: composition restores the better-auth-managed Account destinations;
sys_inbox_messageremains granted by no shipped set (grep-confirmed), so The Account app's Notifications entry 403s for every non-admin — no permission set grants sys_inbox_message #7344's ruled fix stays exactly as necessary — now the only remaining Account 403 rather than one of several. - Scope: no
content/docs/releases/edits, nodocs/adr/**(normal landing path), changeset present (patch ×3, tier justified). Thepackages/spectouch is a prose-only docblock update incontracts/security-service.ts(verified line-by-line) — cross-seat notice posted to the spec seat ([PM seat] domain:spec — ⏳ vacant #6017).
Maintainer veto window remains open until merge; the dispatch rationale (restore of ADR-0090 D5's "baseline ∪ explicit, always") is recorded in the claim comment above.
Generated by Claude Code
- Mechanism confirmed at the site the card presumed:
{ "issue": 7555, "status": "done", "branch": "claude/issue-7555-baseline-compose", "pr": "https://github.com/objectstack-ai/objectstack/pull/7605", "premise_still_valid": true, "summary": "Premise confirmed on origin/main: SecurityPlugin.fallbackPermissionSet is a SINGLE name, so an app's isDefault set displaces the platform member_default at every site that reads it (the additive step and post-resolution fallback in resolvePermissionSetsForContext, resolveFallbackPermissionSets/ADR-0106 D7, the boot everyone-anchor binding, explain attribution). The human baseline is now the LIST it always was — declared set UNION member_default, deduped — via a pure helper composeHumanBaselinePermissionSets() in app-default-permission-set.ts, applied at all five sites; a new security.baselinePermissionSets service carries it and plugin-hono-server's two resolutions (/auth/me/permissions, /me/apps) read it so the UI plane cannot disagree with the data plane. Hard edges held exactly: the agent branch is taken BEFORE the composed list is consulted so principalKind:'agent' keeps its D10 restricted ceiling; fallbackPermissionSet:null still disables the baseline entirely; member_default's grant rows, #5491's narrowing and the D5/D9 high-privilege anchor gate are untouched. The issue's '23 vs 7' breadth figure is from an older commit and was NOT re-confirmed — the mechanism was verified instead, and the fix does not depend on the counts.", "tests": "MEASURED SYMPTOM, before/after on a real booted showcase (fresh sign-up governed by showcase_member_default; same harness, same build path, only composeHumanBaselinePermissionSets ablated to [configured]): sys_account 403->200, sys_session 403->200, sys_api_key 403->200, sys_oauth_application 403->200, sys_user_preference 403->200. sys_inbox_message and sys_notification_receipt read 404 in BOTH columns — not registered on the dogfood showcase stack, so not measurable here and NOT counted as a win. || NEW packages/plugins/plugin-security/src/baseline-composition.test.ts, 11 cases red in four directions (app half / platform half / null half / agent half) plus a guard that member_default really is the only default set granting the discriminator object, so the file cannot go vacuous the way #6964 found its predecessors had. REVERSE VERIFICATION: the same ablation turns exactly 3 of the 11 red — including the runtime case 'the platform half: member_default is STILL in force alongside it' — while the app, null, agent and undeclared-app halves stay green; only the composition-bearing cases move. One case is deliberately NOT a middleware .rejects and says so inline: with zero permission sets the middleware skips its whole CRUD gate by design, so a null-disables case there would be red for an unrelated reason and green either way — it asserts the resolved baseline list instead. || SEMANTICS-FLIP SWEEP: both dogfood displacement pins flipped to assert the composed substance (sys_user_preference -> 200), with their header measurement tables corrected rather than deleted; showcase-d7-default-profile gains a new case proving BOTH sets are bound to the everyone anchor. Refusal pins (agent exclusion, guest tier, anchor gate) verbatim. packages/verify/harness.app-default-profile.test.ts reads the DECLARED name, which still means exactly that — green unchanged. || SUITES (all post-merge of origin/main incl. #7344): plugin-security 46 files / 952 tests passed; plugin-hono-server 16 / 187 passed; rest 82 / 1344 passed; verify 5 / 23 passed; cli 109 / 1182 passed; dogfood FULL gate 87 passed | 1 skipped (88 files), 547 passed | 3 skipped (550 tests). typecheck plugin-security + plugin-hono-server + spec: Done. pnpm check:docs-audit-scope: in sync. node scripts/check-nul-bytes.mjs: OK (7016 files) + targeted grep self-scan clean. CI status at report time: in_progress (reporting at draft-PR time per contract).", "open_questions": [], "out_of_scope_findings": [ "filed as #7608: /auth/me/permissions (:740) and /me/apps (:954) still apply the baseline only when the caller resolves to ZERO sets — the same ADR-0090 D5 fallback cliff the data plane abolished, so a member's first explicit grant silently drops the baseline from the UI's object/tab/capability surface while the engine still enforces it. PR #7605 changes WHICH names those sites resolve and deliberately leaves the guard alone; widening it is a distinct behaviour change for every grant-holding user and deserves its own measurement." ], "notes": { "7344_interaction_asked_explicitly": "#7344 LANDED mid-flight (3987a48) and is merged into this branch; every suite above was re-run on top of it (hence 952 rather than 942 plugin-security tests). Does this change make #7344's fix simpler/harder/unnecessary/unaffected? UNAFFECTED in code — files disjoint, clean merge — but this fix is what lets #7344 REACH the persona it was written for: #7344 adds sys_inbox_message read + an owner-scoped RLS carve-out to member_default, and under displacement member_default resolved for NOBODY in a baseline-declaring app (the showcase included), so that grant would have been invisible to exactly the member #7344's report describes. Complementary; neither is sufficient alone for that persona. Reverse direction: #7344 landing changes nothing here — its grants simply join the composed baseline. One named coupling: baseline-composition.test.ts uses sys_user_preference as its 'granted by member_default and nothing else' discriminator and ASSERTS that property rather than assuming it, so #7344's additions keep it true and a future break is reported by name.", "d7_decision_asked_explicitly": "resolveFallbackPermissionSets (ADR-0106 D7) composes too, documented at the method. Its entire justification is being the SAME resolution the data plane performs; one plane composing while the other displaced would put the two in disagreement about what the baseline is, which is the drift D7 exists to prevent. No-op for every deployment that declares no app baseline (list is exactly ['member_default']); for one that does, guest schema exposure becomes platform UNION app — both deliberate permission-set decisions, rather than an app declaration silently NARROWING a guest's view relative to a deployment that declared nothing.", "changeset": "patch on @objectstack/plugin-security, @objectstack/plugin-hono-server, @objectstack/spec. Patch tier because this restores a stated ADR invariant and can only ADD grants for human principals (most-permissive merge); no API removed, no option changes type, undeclared apps byte-for-byte unaffected.", "labels": "no skip-changeset needed — the PR ships a changeset. Bot labels read back on PR #7605: documentation, size/l, tests, tooling." } }
Generated by Claude Code
Generated by Claude Code
Correction to my ACCEPT comment above, flagged by the dev's report and adopted openly: the line "
sys_inbox_messageremains granted by no shipped set" was true when written (07:37Z) but went stale minutes later — #7344's PR merged mid-flight (3987a48) andmember_defaultnow carries owner-scopedsys_inbox_message/sys_notification_receiptgrants. The dev merged that into this branch and re-ran every suite on top of it (plugin-security 952 tests, all green); the corrected interplay reading is in PR #7605's body and theos-dev-reportcomment: the two fixes are complementary — #7344's grants only reach members of baseline-declaring apps because #7605 composesmember_defaultback in; neither alone closes the showcase symptom.Also noting for the record: the dev's before/after symptom measurement on a booted showcase (5 Account objects 403→200 under composition, ablation-controlled) — and out-of-scope finding #7608 (the same D5 cliff one plane over, in
/auth/me/permissions+/me/appszero-set guards) filed separately, awaiting triage.
Generated by Claude Code
Security-adjacent. The observed failure is fail-closed (403s, no data exposure), but the mechanism — one app's baseline silently displacing the platform baseline for the
everyoneanchor — is a permission-composition defect, and the same displacement decides what an app's members do keep.Symptom
A fresh member (purpose-signed-up, no positions beyond the implicit ones) is served the whole Account app and can use none of it:
So every built-in Account destination is a dead end for that persona, while the nav still advertises all of them.
Expected: either the platform baseline still applies to members of an app that declares its own baseline (so the built-in Account destinations keep working), or the entries whose objects the caller cannot read are pruned from the served nav. Neither happens.
Root cause
The report locates the chain, at this level:
The showcase declares its own baseline set and binds it to the
everyoneanchor. Boot log, verbatim:That binding replaces rather than composes with the platform
member_default. Measured breadth: platformmember_defaultgrants 23 objects — including all the Account ones — whileshowcase_member_defaultgrants 7.Therefore this is not a showcase-specific fixture problem: every built-in Account destination dies for members on any app that declares a baseline.
The second, unprunable half: the Account nav entries are gated with
requiresObject, notrequiredPermissions, so the nav server has no signal with which to prune an entry whose backing object the caller cannot read. The entry is served regardless of the 403 that follows.The report does not name the exact code site of the replace-vs-compose behaviour — it locates it at the ADR-0090 D5
everyone-anchor binding. The presumed home is the baseline/permission-set assembly inpackages/plugins/plugin-security; confirm the site before fixing rather than assuming it from this card.Reproduction
[security] baseline set bound to everyone anchor (ADR-0090 D5).GET /api/v1/data/<object>as that member — all 7 answer 403.Reproduced 2× with two independent member sessions.
Related
sys_jwksis API-disabled and arequiredPermissionsgate cannot prune it #7544 is the sibling half of the same "a nav entry cannot be pruned" gap: there the gate isrequiredPermissionsand the blocker isenable.apiEnabled:false; here the gate isrequiresObjectand the blocker is the baseline replacement. A prune rule that covers only one gate vocabulary will leave the other dead.sys_inbox_message. That is a different cause (an absolute gap in the platform baseline) and its write-up describesmember_defaultas much narrower than the 23 objects measured here. Measure the current baseline contents rather than trusting either number — the two runs are on different framework commits.member_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491 (closed) deliberately narrowedmember_defaultfrom a*wildcard to explicit-allow. This card does not dispute that narrowing; it disputes replacement vs composition when an app binds its own baseline to the same anchor.Source
Extracted from the QA run #7514 (framework a86db17, console 09987b68).