Skip to content

@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

Closed

Description

@os-zhuang

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:2008 reads appDefaultPermissionSetName(config.permissions) and passes it as new SecurityPlugin({ fallbackPermissionSet }).
  • bootStack does not — packages/verify/src/harness.ts defaults to a vanilla new SecurityPlugin() and never reads config.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

  1. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    Claiming this issue.

    Scope is this repo's packages/verify plus 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 make bootStack resolve the app-declared default through the same helper serve.ts uses, with an explicit opt-out for suites that deliberately want the vanilla baseline. Will report measured blast radius across framework-side suites that boot through verify before deciding between a default-flip and an escalation.


    Generated by Claude Code

  2. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    ACCEPT — PR #7091, reviewed against GitHub rather than the report.

    Verified

    • Draft PR targeting main, body opens with Fixes #7001.
    • 9 files. Scope clean: no content/docs/releases/, no docs/adr/**, nothing unrelated to the boot-path asymmetry. Changeset present (.changeset/verify-bootstack-app-default-permission-set.md, plugin-security minor / verify minor / cli patch) and it states the behavioural change rather than just naming it.
    • CI: all 26 check runs success or skipped, including both gate-bearing jobs — ESLint and TypeScript Type Check each reached conclusion: success, not merely "no failure yet".
    • The one CI failure this PR saw was a corepack/pnpm download flake on dogfood shard 1/3 (undici ERR_ASSERTION before 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.ts gave up its open-coded name ? { fallbackPermissionSet: name } : undefined and both boot paths now go through appSecurityPluginOptions(config), so they agree by construction instead of by each caller remembering. Returning undefined rather 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 explicit null means "no baseline at all") is the correct one.

    Two things I checked specifically because they are how this class of fix usually rots:

    1. The parity pin measures a construction, not a mention. The dev's ablation found that reverting harness.ts left the appSecurityPluginOptions import standing, so a toContain('appSecurityPluginOptions') assertion stayed green over a boot path that had stopped calling it. The assertion was changed to walk the new 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.
    2. The blast radius is a measurement, not an estimate. 86 dogfood files / 524 tests, exactly one assertion moved — 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-debt gate initially read @objectstack/verify TEST_DEBT 8 → 9. The cause was a missing .js suffix on a relative import in the new test file — under NodeNext it does not resolve and every symbol it names silently becomes any. 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

  3. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    Landed on main as d19fb5cd6 via PR #7091 (merge queue, 2026-08-09 ~15:3xZ). This card auto-closed on the Fixes line; clearing the stale pm:queue label 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) and packages/plugins/plugin-security/src/app-default-permission-set.ts (+41) — bootStack now resolves the app-declared isDefault set through the same helper the CLI uses, instead of ignoring it.
    • packages/cli/src/commands/serve.ts (+22) and plugin-security/src/index.ts — the CLI side re-pointed at the shared helper rather than its own copy.
    • Coverage: harness.app-default-profile.test.ts (+197 new), serve-verify-security-parity.contract.test.ts (+180 new), plus updates to app-default-permission-set.test.ts and the me-apps-and-everyone-baseline dogfood test.

    The parity contract test is the part worth keeping: it pins serve and bootStack to the same resolution, so the two paths cannot drift apart again silently — which was the actual defect, not the missing default itself.

    ⚠️ Follow-up filed by the reviewing pass, not part of this card and not this seat's lane: #7092 (domain:cli, finding) — standalone-stack.test.ts hand-mirrors appDefaultPermissionSetName and describes the copy as "the exact CLI wiring", which is a conformance claim over a duplicate of the rule. Left unassigned and ungraded; grading finding cards is triage's call, and domain:cli is another seat's queue.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions