Repository navigation
@objectstack/verify's bootStack ignores an app's isDefault permission set, while the CLI honours it — after #5491 a dogfood app cannot test its own declared baseline #7001
Description
Filed by the repo:cloud seat per the cross-seat transfer protocol (file, do not dispatch). Unassigned, pm:queue only — routing left to the triage seat. Found while executing cloud's pin move onto 68feaadd (cloud#1197 / cloud#1219); measured empirically, not inferred.
The asymmetry
Two boot paths disagree about whether an application's declared default profile exists.
- CLI honours it —
packages/cli/src/commands/serve.ts:2008readsappDefaultPermissionSetName(config.permissions)and passes it asnew SecurityPlugin({ fallbackPermissionSet }). bootStackdoes not —packages/verify/src/harness.tsdefaults to a vanillanew SecurityPlugin()and never readsconfig.permissions.
So the profile an app declares is wired when a human runs objectstack dev, and silently absent when the app's own dogfood suite boots it.
Why it matters now, and not before
Before #5491 (9e9445ba, "the platform baseline becomes explicit-allow") the platform's member_default carried an object_permissions["*"] wildcard, so a member with no application profile still reached every object. The gap was invisible: the fallback profile was never load-bearing, because the floor underneath it granted everything anyway.
#5491 removed that floor deliberately (its own measurement: 21 of 21 create-DENIAL probes returned 201; public_read objects answered 200 with ALL rows to non-holders), and its Migration section prescribes exactly one consumer action:
Ship an app default profile. Mark a permission set
isDefault: true… list the objects members legitimately touch, with the axes they need.
That prescription is not expressible through bootStack today. Measured in objectstack-ai/cloud@apps/ee-group-showcase while attempting the prescribed migration: adding definePermissionSet({ isDefault: true, … }) plus permissions: to the app config changed nothing — the same 6 acceptance cases still failed at the object gate. The attempt was reverted rather than worked around.
The only way for a dogfood suite to exercise its own declared baseline is to hand-construct a SecurityPlugin in each suite — which is itself the "custom security code" that several of these apps exist to prove is unnecessary (ADR-0105 Phase 1 acceptance, framework#3541).
Why this is a defect and not a preference
It is a declared ≠ enforced split inside the test harness that exists to catch that split. An app can declare a profile, ship it to users through the CLI path, and have every one of its own tests run against a boot that does not include it — green tests, different production behaviour. The class is the same one #5852 and framework#3695 came from: the context the tests feed is not the context the real seam produces.
Suggested shape (not a ruling — the harness is not this seat's to design)
Make bootStack resolve the app-declared default the same way serve.ts does, so the two boot paths agree by construction rather than by each caller remembering. If a suite deliberately wants the vanilla baseline, that should be the explicit opt-out, not the silent default. A conformance-style assertion that "the profile bootStack wires equals the profile serve wires for the same config" would keep the two from drifting apart again.
Blast radius
Every apps/*-showcase dogfood suite in objectstack-ai/cloud, and any framework-side suite booting through verify that relies on an app profile after #5491. Consumer context and the full three-axis analysis of the cloud-side half are on objectstack-ai/cloud#1219 (that card is needs-user-decision for a separate reason — which control-plane objects members may read — and this gap is one of the two sub-questions it names).
Dedup: searched open issues for bootStack / fallbackPermissionSet / verify harness SecurityPlugin isDefault — zero hits before filing.
Refs: objectstack-ai/cloud#1219, objectstack-ai/cloud#1197, #5491, #5492, #3541, ADR-0105.
Activity
os-project-manager commentedon Aug 9, 2026
Claiming this issue.
session_01F8q5J1MQyocgtNspb15fSnclaude/issue-7001-bootstack-app-default-permission-setdomain:devxPM seat (archive issue [PM seat] domain:devx @ objectstack — 🟢 os-warren · session_01FM29ZqLTrnPNXxijioZwMp #6023).Scope is this repo's
packages/verifyplus whatever shared helper the resolution logic ends up living in. The cloud-side consumer work (objectstack-ai/cloud#1219) is out of scope here.Plan: re-verify both anchor points on current
main, reproduce the asymmetry as a red test, then makebootStackresolve the app-declared default through the same helperserve.tsuses, with an explicit opt-out for suites that deliberately want the vanilla baseline. Will report measured blast radius across framework-side suites that boot throughverifybefore deciding between a default-flip and an escalation.Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
ACCEPT — PR #7091, reviewed against GitHub rather than the report.
Verified
main, body opens withFixes #7001.content/docs/releases/, nodocs/adr/**, nothing unrelated to the boot-path asymmetry. Changeset present (.changeset/verify-bootstack-app-default-permission-set.md,plugin-securityminor /verifyminor /clipatch) and it states the behavioural change rather than just naming it.successorskipped, including both gate-bearing jobs — ESLint and TypeScript Type Check each reachedconclusion: success, not merely "no failure yet".corepack/pnpmdownload flake on dogfood shard 1/3 (undiciERR_ASSERTIONbefore any test ran), which took the aggregate gate down with it for want of a shard attestation. The retry passed; the signature is infrastructure, not this diff.What earns the accept beyond "it compiles"
The convergence is real rather than cosmetic:
serve.tsgave up its open-codedname ? { fallbackPermissionSet: name } : undefinedand both boot paths now go throughappSecurityPluginOptions(config), so they agree by construction instead of by each caller remembering. Returningundefinedrather than{ fallbackPermissionSet: undefined }is argued as a decision, not a style choice, and the reason given (an absent key means "derive from the built-ins", an explicitnullmeans "no baseline at all") is the correct one.Two things I checked specifically because they are how this class of fix usually rots:
harness.tsleft theappSecurityPluginOptionsimport standing, so atoContain('appSecurityPluginOptions')assertion stayed green over a boot path that had stopped calling it. The assertion was changed to walk thenew SecurityPlugin(argument with balanced parens — the naive\(([^)]*)\)form truncates at the inner)and compares equal on both sides while measuring nothing. That is measured, not reasoned, and it is the difference between a pin and a decoration.me-apps-and-everyone-baseline, whose header already read "Deliberately VANILLA". Its dependence on the harness default was real but silent; it is now written into the argument (bootStack(config, { security: new SecurityPlugin() })) with the claim itself unchanged. That is the issue's "explicit opt-out rather than silent default", landing on the one file that actually had the dependency.Noted, no action: the
type-check-debtgate initially read@objectstack/verifyTEST_DEBT 8 → 9. The cause was a missing.jssuffix on a relative import in the new test file — underNodeNextit does not resolve and every symbol it names silently becomesany. Fixed at the cause; the count returned to 8 rather than the ratchet being raised.Out of scope, as stated: the cloud-side consumer (objectstack-ai/cloud#1219) is not this PR's, and #7002 (
data-service.mdx) has no file overlap — confirmed.Flipping to ready and arming auto-merge; it lands through the merge queue.
Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
Landed on
mainasd19fb5cd6via PR #7091 (merge queue, 2026-08-09 ~15:3xZ). This card auto-closed on theFixesline; clearing the stalepm:queuelabel that a closed card should not carry.Artifact verified in
origin/main, not inferred from the PR page — 9 files, changeset present:packages/verify/src/harness.ts(+33) andpackages/plugins/plugin-security/src/app-default-permission-set.ts(+41) —bootStacknow resolves the app-declaredisDefaultset through the same helper the CLI uses, instead of ignoring it.packages/cli/src/commands/serve.ts(+22) andplugin-security/src/index.ts— the CLI side re-pointed at the shared helper rather than its own copy.harness.app-default-profile.test.ts(+197 new),serve-verify-security-parity.contract.test.ts(+180 new), plus updates toapp-default-permission-set.test.tsand theme-apps-and-everyone-baselinedogfood test.The parity contract test is the part worth keeping: it pins
serveandbootStackto the same resolution, so the two paths cannot drift apart again silently — which was the actual defect, not the missing default itself.domain:cli,finding) —standalone-stack.test.tshand-mirrorsappDefaultPermissionSetNameand describes the copy as "the exact CLI wiring", which is a conformance claim over a duplicate of the rule. Left unassigned and ungraded; gradingfindingcards is triage's call, anddomain:cliis another seat's queue.Generated by Claude Code
test(runtime): pin standalone-stack's app-default resolution against …
fix(plugin-security): the app default permission set resolves from `p…
fix(plugin-security): the app default permission set resolves from `p…