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
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 everyone anchor — 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:
- All 10 Account-app nav ids are served to the member.
- 7/7 of the objects backing those entries answer 403 on read.
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:[security] baseline set bound to everyone anchor (ADR-0090 D5) -
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 in packages/plugins/plugin-security; confirm the site before fixing rather than assuming it from this card.
Reproduction
- Boot the showcase app; confirm the boot log line
[security] baseline set bound to everyone anchor (ADR-0090 D5). - Sign up a fresh member and sign in.
- Read the served nav for the Account app — all 10 nav ids are present.
- For each of the 7 objects backing those entries,
GET /api/v1/data/<object>as that member — all 7 answer 403.
Reproduced 2× with two independent member sessions.
Related
- Setup › Advanced › Signing Keys (JWKS) is a dead nav entry —
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. - The Account app's Notifications entry 403s for every non-admin — no permission set grants sys_inbox_message #7344 (open) reports the Account app's Notifications entry 403ing for every non-admin because no shipped permission set grants
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).
Activity
os-help commentedon Aug 11, 2026
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
os-help commentedon Aug 11, 2026
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
os-help commentedon Aug 11, 2026
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):
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.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.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).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.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 — 🟢 os-bill · session_01T1Y1g92DfSEz93iuCisju4 #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
os-help commentedon Aug 11, 2026
{ "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
os-help commentedon Aug 11, 2026
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
fix(security): compose an app-declared baseline WITH the platform mem…
feat(spec,plugin-security): publish the caller's resolved permission …