From 79222a3b2e0f4c80de73dc43343a467caae31378 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 06:39:11 +0000 Subject: [PATCH 1/3] =?UTF-8?q?docs(qa):=20RUNNER=20walled-boot=20entry=20?= =?UTF-8?q?=E2=80=94=20the=20serve-mode=20auth=20secret,=20the=20full-copy?= =?UTF-8?q?=20scratch=20app,=20the=20admin=20persona=20door;=20the=20setsi?= =?UTF-8?q?d=20stop=20recipe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/RUNNER.md | 77 ++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/docs/qa/platform-checklist/RUNNER.md b/docs/qa/platform-checklist/RUNNER.md index ea3386044c2..d9a445ce3a0 100644 --- a/docs/qa/platform-checklist/RUNNER.md +++ b/docs/qa/platform-checklist/RUNNER.md @@ -302,6 +302,25 @@ contradicts it, and correct it here when it does. the real port). This is "zero hits needs a positive control" applied to one tool: a negative from a command that never ran is not evidence. +- **A SIGTERM to the `pnpm` wrapper does not stop the server under it — start a + backgrounded boot with `setsid` and stop its process group.** The canonical boot (in the + `--no-ui` entry below) is `pnpm -C … exec objectstack dev …`: pnpm spawns `os dev`, which + spawns `serve --dev` (`packages/cli/src/commands/dev.ts`). `os dev` forwards a SIGINT or + SIGTERM sent to its own pid to that child (its `process.on('SIGTERM', …)` handler calls + `beginShutdown()` of `ServeRestartCoordinator`, `packages/cli/src/utils/dev-restart.ts`), + but nothing forwards one sent to pnpm, so `kill` on the pnpm pid leaves the `serve` + grandchild listening. Both runners of the 18.0 sweep's round 3 met it (wave anchor + #24579 §6). Measured at + `46bb5e3e54` with a stand-in tree, `pnpm -C examples/app-showcase exec node parent.js` + where `parent.js` forwards SIGTERM to an HTTP child as `os dev` does: two seconds after a + SIGTERM to the pnpm pid the child still answered 200, and a SIGTERM to the process group + stopped all four processes. ⚠️ `$!` is not the group id: a backgrounded `setsid` forks + when the shell made it a group leader, so `$!` names a process that has already exited. + **So:** `setsid pnpm -C … exec objectstack dev … > LOG 2>&1 &`; once the port answers, + read the group from the listener, `PGID=$(ps -o pgid= -p "$(lsof -ti tcp:PORT)" | tr -d ' ')`; + stop it with `kill -TERM -- -$PGID`, and confirm `lsof -ti tcp:PORT` then prints nothing. + ⛔ Never stop a server by name (`pkill`), and never a group you did not start. + - **Console session auth is a bearer token in `localStorage`, and a form sign-in ALSO sets a cookie — you need both halves.** The console's auth client stores the session under `auth-session-token` (objectui `packages/auth/src/createAuthClient.ts`: @@ -466,17 +485,67 @@ contradicts it, and correct it here when it does. So on 18.0.0 `OS_TENANCY_POSTURE=group` or `isolated` (or the legacy `OS_MULTI_ORG_ENABLED=true`, which `resolveTenancyPosture()` in `packages/types/src/env.ts` reads as `isolated`) boots through - `os serve`, and so `os dev` and `os start`. Each prerequisite below refuses the boot with a - FATAL naming itself when absent, so a refusal tells the runner what is missing, never that - the posture is unsupported: + `os serve`, and so `os dev` and `os start`. Through `os dev`, each prerequisite below + refuses the boot with a FATAL naming itself when absent (QA run #24783 measured all three), + so a refusal tells the runner what is missing, never that the posture is unsupported: 1. the multi-org runtime **declared by the app**: `@objectstack/organizations` in the app's own `package.json` and installed there. Reachable only through `NODE_PATH` or a hoisted store is refused on purpose (ADR-0093 D5). No example app declares it, so a walled run - uses a scratch app that does; + uses a scratch app that does: a **full copy** of `examples/app-showcase`, `src/` + included (without `node_modules`, `.objectstack` or `dist`), whose own `package.json` + carries every showcase dependency as a `link:` to its workspace package plus + `"@objectstack/organizations": "link:/packages/plugins/organizations"`, installed + from the copy with `pnpm install --ignore-workspace` (QA run #24783). A scratch config + that only re-exports the showcase's `objectstack.config.ts` does not boot: a package-file + reference resolves against the root being served, and the showcase's OpenAPI connector + names its document that way (`spec: './src/system/connectors/status-openapi.json'` in + `examples/app-showcase/src/system/connectors/index.ts`, read relative to the package + root at materialization, where a missing document fails the boot); 2. `OS_PLATFORM_OWNER_EMAIL`, the deployment's platform administrators: first-registrant promotion is off under a wall; 3. a declared membership policy, `OS_AUTH_MEMBERSHIP_POLICY` (for example `invite-only`). + ⚠️ **A bare `os serve` in production mode judges none of the three until it has an auth + secret.** Without `--dev`, and with `NODE_ENV` anything but `development`, `serve` composes + the auth family only when `OS_AUTH_SECRET` (or a legacy spelling) resolves — + `resolvePlatformAuthComposition()` in `packages/core/src/stack-auth.ts`, reason + `no-secret` — and all three checks sit behind it: the multi-org runtime is loaded inside + `serve.ts`'s auth-composition branch, the owner-email check runs in `AuthPlugin`'s `init()` + (`packages/plugins/plugin-auth/src/auth-plugin.ts`), and the membership-policy gate skips a + kernel with no `auth` service (`assertWalledMembershipPolicyDeclared()` in + `packages/plugins/organizations/src/membership-policy-gate.ts`). With the secret unset the + boot prints `⚠ AuthPlugin skipped — set OS_AUTH_SECRET …` and comes up + `⚠ Server is ready — DEGRADED: missing core services: auth` (`printServerReady()` in + `packages/cli/src/utils/format.ts`), not a FATAL: QA run #24783 measured it with + `os serve --no-ui` from the scratch app, once with prerequisite 2 absent and once with + prerequisite 3 absent. A production `serve` also needs a stable encryption key, + `OS_SECRET_KEY` or a key file an earlier boot persisted; without one `LocalCryptoProvider` + refuses the boot naming `OS_SECRET_KEY` + (`packages/services/service-settings/src/local-crypto-provider.ts`). `os dev` needs + neither: it spawns `serve --dev`, whose development boot falls back to + `DEV_AUTH_SECRET_FALLBACK` (`resolveAuthSecret()`, same `stack-auth.ts`) and persists a key. + `os start` hands its `serve` child an auth secret (its `--auth-secret` flag, the + environment, or the `auth-secret` file it persists in its home directory) and opts the + provider into a persisted key (`OS_CRYPTO_AUTOKEY`), in `packages/cli/src/commands/start.ts` + — read from the code, not booted. **So:** boot a walled run through `os dev`, or give a bare + `serve` `OS_AUTH_SECRET` and a stable key first. ⛔ Never read a DEGRADED walled boot as + "the prerequisites are met", and never score a walled clause on it. + + **Walled personas come from the admin door.** The walled boot keeps self-registration + closed: the showcase declares no `auth.audience`, so `POST /api/v1/auth/sign-up/email` + answers **`403 SELF_REGISTRATION_CLOSED`** exactly as on a stock boot + (`decideAudienceAdmission()` in `packages/plugins/plugin-auth/src/audience-posture.ts`; the + entry "A stock showcase boot refuses self-registration" above). QA run #24783 minted every + walled persona as the operator (the seeded admin, the address `OS_PLATFORM_OWNER_EMAIL` + names) with `POST /api/v1/auth/admin/create-user` (`runAdminCreateUser()` in + `packages/plugins/plugin-auth/src/admin-user-endpoints.ts`), then had each persona + `POST /api/v1/auth/change-password`. Under `invite-only` each lands with + `membershipCreated: false` and in no organization (the reconciler's `policy-skip`, + `reconcileMembership()` in `packages/plugins/plugin-auth/src/reconcile-membership.ts`); a + persona that needs an organization creates its own with + `POST /api/v1/auth/organization/create` and switches to it with `set-active` (the next + entry). + Measured at `45898dbf6`: a scratch app declaring the real runtime, booted by the built CLI under `isolated` with the other two set, mounted `Organizations`, answered `GET /api/v1/discovery` 200 and logged no system-write refusal. The seven walled From 8a5a8b06601318ed12af2acd3462b9aa81138ef8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 06:46:52 +0000 Subject: [PATCH 2/3] =?UTF-8?q?docs(qa):=2018.0=20sweep=20round=2011=20?= =?UTF-8?q?=E2=80=94=20the=20walled=20pass's=20checklist=20findings=20F3?= =?UTF-8?q?=E2=80=93F8=20on=20eight=20items,=20scope-depth's=20pin=20dropp?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/access-security.json | 65 +++++++++++++------ .../platform-checklist/areas/api-backend.json | 10 ++- .../areas/identity-auth.json | 16 +++-- .../areas/integration-system.json | 10 ++- .../areas/platform-core.json | 15 +++-- 5 files changed, 83 insertions(+), 33 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index a9d7c42492d..5a2c2f5c6fb 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -831,7 +831,7 @@ "title": "Scope depth (readScope/writeScope): org-wide read with own-only write, per persona — depth widens along geometry, never bypasses", "since": "v15", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -845,6 +845,9 @@ "requires": [ "showcase_inquiry is OWD private with create granted to the baseline — depth only matters where the baseline stops (permission-sets.ts docblock)", "personas staffed by granting the sets (sys_user_permission_set) or assigning the positions manager/exec/ops (each declares its set in permissionSets, examples/app-showcase/src/security/positions.ts)" + ], + "knownGaps": [ + "No permanent pin scores a clause of this item, so it carries no `automated` field and every clause is driven by hand. The three dogfood suites nearest to it, packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts, packages/qa/dogfood/test/showcase-scope-depth-write.dogfood.test.ts and packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts, assert the business-unit depth axis on showcase notes (readScope unit / unit_and_below / own_and_reports, writeScope unit, the absent-writeScope default, the fallback profile and the open edition's fail-closed degrade). None of them drives the manager / executive / ops personas or showcase_inquiry, so none asserts the org-read / own-write asymmetry acceptance[0]–[5] score. `automated` returns when a pin asserts one of those clauses in full" ] }, "steps": [ @@ -902,10 +905,6 @@ "executive: read org / write none", "ops: read org / write org" ], - "automated": { - "kind": "dogfood", - "ref": "packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts + packages/qa/dogfood/test/showcase-scope-depth-write.dogfood.test.ts + packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts — ⚠️ ADJACENT pins, not this item's: they drive the business-unit depth values (readScope unit / unit_and_below / own_and_reports, writeScope unit, the absent-writeScope default and the fallback profile), not the org-read / own-write asymmetry of the manager, executive and ops personas that acceptance[0]–[5] score, so no clause here is satisfied by running them — drive the clauses by hand" - }, "traps": [ "wrong-persona" ], @@ -927,6 +926,12 @@ "date": "2026-10-19", "change": "steps[0] and automated.ref corrected — checklist-accuracy findings of the 18.0 QA sweep (QA run #24559, access-security.scope-depth-asymmetry; wave anchor #24579 §3; round 2's Tier 1 record #24598, which names this item among the refs that over-claim clause coverage). steps[0] POSTed a showcase_inquiry with `subject`; the object has no such field and requires name, email and message (examples/app-showcase/src/data/objects/inquiry.object.ts). The three dogfood pins exercise the business-unit depth axis (unit / unit_and_below / own_and_reports and the fallback profile), not the manager / executive / ops org-read and own-write asymmetry the clauses score, so automated.ref now says so and that no clause rests on them. No clause changed", "ref": "#24632" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "automated dropped and fixtures.knownGaps[0] added, under round 6's theme-B rule (drop `automated` where no pin asserts a full clause, with a knownGaps line) — round 11 of wave anchor #24579, whose round-3 summary named this nominally Tier 1 item. Its revision-3 automated.ref already said the three dogfood pins cover no clause. Re-read at 46bb5e3e54: they drive business-unit depth on showcase notes and never the manager / executive / ops personas or showcase_inquiry. The item is now Tier 2, every clause driven by hand as revision 3 already prescribed. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round11" } ] }, @@ -2347,24 +2352,26 @@ "title": "A session with no active organization is a LEGAL state with fail-closed semantics: tenant-scoped reads resolve to nothing, tenant-scoped writes are refused loudly naming the missing org, and nothing is ever NULL-stamped", "since": "v17", "status": "active", - "revision": 5, + "revision": 6, "priority": "P0", "surface": "api", "personas": [ - "a freshly signed-up user on their FIRST session (structurally predates their sys_member row)", - "a user removed from their organization", + "a user holding NO membership (producer (1) of fixtures.knownGaps[0]): on the walled invite-only boot, minted by the operator with POST /api/v1/auth/admin/create-user, which answers membershipCreated:false", + "an organization member whose session has cleared its active organization (producer (2) of fixtures.knownGaps[0]): POST /api/v1/auth/organization/set-active {\"organizationId\": null}", "seeded admin (control — has an active organization)" ], "fixtures": { "app": "showcase", "requires": [ "a tenancy-walled deployment (ADR-0095 Layer 0 active on at least one tenant-scoped object) — confirm the posture before scoring, since on an unwalled deployment every clause here is vacuous", - "the ability to complete a signup and use the resulting first session before the membership reconciler settles, OR to remove a member and reuse their live session" + "one of the two durable producers fixtures.knownGaps[0] names, on that walled boot: a user minted by POST /api/v1/auth/admin/create-user under OS_AUTH_MEMBERSHIP_POLICY=invite-only (membershipCreated:false, no sys_member row), or a member who sends POST /api/v1/auth/organization/set-active {\"organizationId\": null}" ], "knownGaps": [ - "The state is timing-derived on the signup path (better-auth defers the membership write past the signup transaction, ADR-0093), so reproducing it by racing signup is flaky. The DURABLE reproduction is the removed-member path, or a multi-organization deployment whose reconciler binds nobody (ADR-0093 D1 `no-target-org`). Record WHICH producer the run used — a clause proven only on a racy producer is weaker evidence and must say so.", - "No stock showcase fixture is known to configure `invite-only` or SSO-JIT-pending; those two producers are blocked(fixture) unless the run configures them.", - "requires[0]'s tenancy-walled deployment is a walled boot. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses the boot naming itself. The item stays blocked (fixture, #9334) for the producer reasons above, not for the posture; its pin (automated.ref) boots the posture in-process." + "The state needs a producer, and two are durable and non-racy on the walled boot (knownGaps[2]), both measured by QA run #24783 on `isolated` and `group`. (1) NO membership: under OS_AUTH_MEMBERSHIP_POLICY=invite-only a user minted by POST /api/v1/auth/admin/create-user answers membershipCreated:false and holds no sys_member row, because the reconciler answers `policy-skip` under invite-only (packages/plugins/plugin-auth/src/reconcile-membership.ts#reconcileMembership, packages/plugins/plugin-auth/src/admin-user-endpoints.ts#bindUserToSoleOrganization), so every session it mints reads activeOrganizationId null. (An explicitly declared `auto` binds nobody under a wall either, ADR-0093 D1 `no-target-org`; no run has measured that boot.) (2) A member who CLEARS its active organization: POST /api/v1/auth/organization/set-active {\"organizationId\": null}, after which GET /api/v1/auth/get-session reads null. The two differ under `group`, where the scope is the membership set (acceptance[0]), so record WHICH producer each clause was scored on. Two former producers no longer yield the state. The signup race is gone: the session seam settles a newly created user's membership before its first session resolves an organization (ADR-0123 D3; packages/plugins/plugin-auth/src/auth-manager.ts#composeDatabaseHooks, its defaultActiveOrg hook), so only a policy that binds nobody leaves that session without one. The removed-member path ends the session's claim instead: removing a user's last membership revokes each live session that named that organization, and a user still holding another membership has such a session re-pointed to it (packages/plugins/plugin-auth/src/membership-ended-session.ts#endSessionClaimsForEndedMembership); QA run #24783 measured the removed member's token answering 401 UNAUTHENTICATED", + "`invite-only` is no longer a fixture gap: the walled boot must declare a membership policy anyway (RUNNER.md 'A walled boot is a fixture of its own', prerequisite 3), and declaring `invite-only` gives knownGaps[0]'s producer (1). SSO-JIT-pending still has no stock fixture: that producer is blocked(fixture) unless the run configures SSO.", + "requires[0]'s tenancy-walled deployment is a walled boot. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses an os dev boot naming itself, while a bare os serve in production mode judges none of them until OS_AUTH_SECRET is set (that entry). The item stays blocked (fixture, #9334) because every clause is a walled leg and the stock showcase provisions no walled boot, not for want of a producer (knownGaps[0]); its pin (automated.ref) boots the posture in-process.", + "acceptance[4] has an open question for a user who holds NO membership at all (producer (1) of knownGaps[0]): its login/logout rows in sys_audit_log carry tenant_id null, because no organization exists to stamp and ADR-0123 D3's ordering fix 'must not manufacture a membership that policy says must not exist'. QA run #24783 read those rows only as the platform operator (GET /api/v1/data/sys_audit_log filtered by user_id answered 200), while the user itself gets 403 on sys_audit_log, on both postures. ADR-0123 does not state whether that is the intended answer: D3 forbids a NULL-tenanted row under this state 'in any subsystem' and pays the audit ledger by ordering, which binds nobody under invite-only. The clause stands as written. Until the question is ruled (raised by QA run #24783's finding F3 for wave anchor #24579; no card carries it yet), score acceptance[4] on that user from what is measured (the raw rows, their tenant_id and the readers that see them) and name this question beside the verdict", + "On `group`, producer (2), a member with no active organization, gets 500 {\"code\": \"ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED\"} on a tenant-scoped create, where `isolated` and a member-less caller answer the acceptance[1] 403 naming the missing organization; nothing is written (QA run #24783: acceptance[1], acceptance[5], acceptance[6] and negative[3] FAIL on `group` only). That is the open product card #24784. The clauses stand as written: score them against it until it lands, and score the member-less caller's write separately" ] }, "blocked": { "by": "fixture", "ref": "#9334" }, @@ -2384,9 +2391,9 @@ ], "acceptance": [ { - "clause": "tenant-scoped READS resolve to nothing — an empty result set, NOT an error: Layer 0's deny sentinel (RLS_DENY_FILTER) yields zero rows and a 200", + "clause": "tenant-scoped READS resolve to nothing — an empty result set, NOT an error: Layer 0's deny sentinel (RLS_DENY_FILTER) yields zero rows and a 200 — for a caller with no organization scope, which under `isolated` is no active organization and under `group` is an EMPTY membership set (ADR-0123 D2). Under `group` membership, not the active organization, bounds read reach (ADR-0105 D2), so a member with no active organization reads the rows of its own memberships: that read is the posture's union wall, not a leak (packages/plugins/plugin-security/src/tenant-layer.ts#computeTenantLayer0Filter)", "oracle": "api", - "verify": "GET returns 200 with an empty collection; a 4xx/5xx here is a FAIL (the read side is deliberately not an error path)", + "verify": "GET returns 200 with an empty collection for the scope-less caller — under `isolated` either producer of fixtures.knownGaps[0], under `group` producer (1) only; a 4xx/5xx here is a FAIL (the read side is deliberately not an error path). Under `group`, producer (2) answers 200 with rows of its own memberships' organizations only: a row of an organization it does not belong to is a FAIL (acceptance[3]), and its own organizations' rows are not", "evidence": "status + body" }, { @@ -2473,6 +2480,12 @@ "date": "2026-10-20", "change": "fixtures.knownGaps[2]'s release attribution corrected — round 10 of wave anchor #24579. The line said the walled boot refusal was 18.0's ('the 18.0 refusal') and that 18.x step 4 (#24580) deleted it, so a runner on 18.0.0 could not tell whether its release boots the walled posture. Under ruling record 6282882793 on the v18 epic (items 1 and 2), 18.0.0 is published only after step 4 has merged, and a walled posture boots on it. Of the published @objectstack/cli prereleases only 18.0.0-next.5 carries serve.ts walledPostureRefusal: next.0 to next.3 predate it, measured in each npm tarball and at each version tag, and no next.4 was published. Only that attribution changed: no clause, step or other knownGap moved", "ref": "#24788" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "producers re-measured and acceptance[0] aligned with its governing text — QA run #24783 (finding F3), round 11 of wave anchor #24579. personas[0]–[1], fixtures.requires[1] and knownGaps[0]–[1] now name the two durable producers on a walled boot (a user the operator creates under invite-only, membershipCreated:false; a member that sets its active organization to null). The signup race is closed by ADR-0123 D3's session-seam settle, and the removed-member path revokes or re-points the session (QA run #24783 measured 401). acceptance[0]'s clause and verify scope 'resolve to nothing' to a caller with no organization scope, which ADR-0123 D2 defines under `group` as an empty membership set, ADR-0105 D2 letting a `group` member's reads span its memberships. acceptance[4] is unchanged: ADR-0123 states no answer for a member-less user's NULL-tenanted audit rows, so knownGaps[3] names the question. knownGaps[4] names #24784, the `group` write 500. knownGaps[2]: the prerequisite sentence holds through os dev only (a bare production os serve needs OS_AUTH_SECRET first, RUNNER.md), and the blocked marker's reason is restated (every clause is a walled leg) because a producer is no longer missing; the marker itself is unchanged", + "ref": "claude/issue-24579-v18-sweep-round11" } ] }, @@ -2481,7 +2494,7 @@ "title": "Activation-ledger write authority (ADR-0126 §5): the manage_metadata tier refuses first on every posture, and the shared operator gate — INERT on stock `single` — demands the platform_admin POSITION in walled postures, at both the flow and action doors", "since": "v17", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "api", "personas": [ @@ -2496,10 +2509,10 @@ "stock showcase boots the `single` tenancy posture (no tenancy service / ADR-0093 D4-D5 fallback) — CONFIRM the posture before scoring, because it decides which gate can answer at all. No API exposes the posture; the proof is the boot banner line `Tenancy: single` (printed at every log level), corroborated at OS_LOG_LEVEL=info by the log line \"[security] tenancy posture 'single' — Layer 0 is inert …\"", "a packaged flow: showcase_urgent_task_alert (com.example.showcase, examples/app-showcase/src/automation/flows/index.ts) for POST /api/v1/automation/showcase_urgent_task_alert/toggle", "a packaged action for POST /api/v1/actions/_activation/showcase_task/showcase_mark_done (examples/app-showcase/src/ui/actions/index.ts)", - "a runtime-authored scratch permission set with system_permissions ['manage_metadata'] granted to a fresh member via a sys_user_permission_set row (same runtime-authoring pattern access-security.fls-mask-and-strip's read half uses) — no stock showcase set grants manage_metadata" + "a runtime-authored scratch permission set with system_permissions ['manage_metadata'] granted to a fresh member via a sys_user_permission_set row (same runtime-authoring pattern access-security.fls-mask-and-strip's read half uses) — no stock showcase set grants manage_metadata. The walled legs (knownGaps[0]; steps[8], acceptance[4]) need that set held by an organization OWNER inside the owner's own organization, and no HTTP door places that row on a walled boot (QA run #24783): the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, 403 '… organization_id is outside the caller's organization scope' (packages/plugins/plugin-security/src/security-plugin.ts#denyTenantPlacement), and the owner's own insert is refused by ADR-0090 D12 condition (c), because manage_metadata is declared scope 'platform' (packages/spec/src/security/capabilities.ts; packages/plugins/plugin-security/src/delegated-admin-gate.ts#organizationBoundaryFailure). Use the dogfood harness's recipe instead: the grant row {user_id, permission_set, organization_id} is a system-context engine insert stamped with that organization, as packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts#boot places it, or a direct row on the run's own scratch DB, as QA run #24783 placed it. Record it in the run record as a fixture write made outside HTTP" ], "knownGaps": [ - "The group/isolated legs (clause 5, acceptance[4]; steps[8]; variants[1]–[2]) run live on a walled boot, which a stock showcase boot is not (fixtures.requires[0] confirms `single`). On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses the boot naming itself. A run that does not provision it records those legs blocked(fixture) naming that entry, never fail. Clause 5 is also scored from the unit pins in automated.ref (oracle test), which drive the posture x identity matrix in-process. Never fake a posture to tick them.", + "The group/isolated legs (clause 5, acceptance[4]; steps[8]; variants[1]–[2]) run live on a walled boot, which a stock showcase boot is not (fixtures.requires[0] confirms `single`). On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses an os dev boot naming itself, while a bare os serve in production mode judges none of them until OS_AUTH_SECRET is set (that entry). A run that does not provision it records those legs blocked(fixture) naming that entry, never fail. Clause 5 is also scored from the unit pins in automated.ref (oracle test), which drive the posture x identity matrix in-process. Never fake a posture to tick them.", "⚠️ On the STOCK `single` posture the §5 operator gate is INERT by design (activation-gate.ts — postureEnforcesWall('single') is false, so the gate returns before the position is ever read). The refusal an unentitled caller gets on stock showcase is the manage_metadata AUTHORING TIER, not the operator gate — score clause 1/2 against the tier and never against §5, or the run records the wrong gate as proven." ] }, @@ -2622,6 +2635,12 @@ "date": "2026-10-20", "change": "fixtures.knownGaps[0]'s release attribution corrected — round 10 of wave anchor #24579. The line said the walled boot refusal was 18.0's ('the 18.0 refusal') and that 18.x step 4 (#24580) deleted it, so a runner on 18.0.0 could not tell whether its release boots the walled posture. Under ruling record 6282882793 on the v18 epic (items 1 and 2), 18.0.0 is published only after step 4 has merged, and a walled posture boots on it. Of the published @objectstack/cli prereleases only 18.0.0-next.5 carries serve.ts walledPostureRefusal: next.0 to next.3 predate it, measured in each npm tarball and at each version tag, and no next.4 was published. Only that attribution changed: no clause, step or other knownGap moved", "ref": "#24788" + }, + { + "revision": 8, + "date": "2026-10-20", + "change": "fixtures.requires[3] names the recipe for the walled legs' org-owner manage_metadata grant, and knownGaps[0]'s prerequisite sentence says os dev — QA run #24783 (findings F7 and F2), round 11 of wave anchor #24579. No HTTP door placed the grant on the walled boot: the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, and the owner's own insert by ADR-0090 D12 condition (c), manage_metadata being scope 'platform'. The recipe is the dogfood harness's system-context engine insert (wall-environment-metadata-refusal.dogfood.test.ts, its boot helper). A bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md), so the refusal sentence now names os dev. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round11" } ] }, @@ -3415,7 +3434,7 @@ "title": "Under a walled posture (group / isolated) environment metadata and the security catalog belong to the platform operator: an org owner holding manage_metadata is refused 403 on every /meta write verb, every /packages write, install-local of a catalog-carrying package, _migrate-stored and POST /automation — and the single posture is unchanged", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P0", "surface": "api", "personas": [ @@ -3428,10 +3447,10 @@ "requires": [ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", "a second boot with OS_TENANCY_POSTURE=isolated (walled posture) on its own port and file DB — the stock single posture leaves every walled clause vacuous, so confirm the posture from the boot log before scoring", - "on the walled boot: a second organization whose owner is granted manage_metadata but not the platform_admin position" + "on the walled boot: a second organization whose owner is granted manage_metadata but not the platform_admin position. No HTTP door places that grant on a walled boot (QA run #24783): the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, 403 '… organization_id is outside the caller's organization scope' (packages/plugins/plugin-security/src/security-plugin.ts#denyTenantPlacement), and the owner's own insert is refused by ADR-0090 D12 condition (c), because manage_metadata is declared scope 'platform' (packages/spec/src/security/capabilities.ts; packages/plugins/plugin-security/src/delegated-admin-gate.ts#organizationBoundaryFailure). The recipe is the dogfood harness's: the operator saves a permission set carrying systemPermissions ['manage_metadata'], and the grant row {user_id, permission_set, organization_id} is a system-context engine insert stamped with that organization, as packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts#boot places it, or a direct row on the run's own scratch DB, as QA run #24783 placed it. Record it in the run record as a fixture write made outside HTTP" ], "knownGaps": [ - "Every live leg here needs the walled boot fixtures.requires[1] names and the second organization fixtures.requires[2] names. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses the boot naming itself. On that boot the second organization is created through POST /api/v1/auth/organization/create (RUNNER.md 'A stock `single` boot holds exactly one organization, and no door mints a second; a walled boot mints them through the auth API'). A run that does not provision them records acceptance[1], acceptance[2] and negative[0] blocked(fixture) naming those entries, never fail. steps[3]'s single-posture admit is only the control for the walled refusal and proves nothing alone. acceptance[0]'s pins boot the walled posture at kernel level and stay the item's pinned evidence" + "Every live leg here needs the walled boot fixtures.requires[1] names and the second organization fixtures.requires[2] names. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses an os dev boot naming itself, while a bare os serve in production mode judges none of them until OS_AUTH_SECRET is set (that entry). On that boot the second organization is created through POST /api/v1/auth/organization/create (RUNNER.md 'A stock `single` boot holds exactly one organization, and no door mints a second; a walled boot mints them through the auth API'). A run that does not provision them records acceptance[1], acceptance[2] and negative[0] blocked(fixture) naming those entries, never fail. steps[3]'s single-posture admit is only the control for the walled refusal and proves nothing alone. acceptance[0]'s pins boot the walled posture at kernel level and stay the item's pinned evidence" ] }, "steps": [ @@ -3500,6 +3519,12 @@ "date": "2026-10-20", "change": "fixtures.knownGaps[0]'s release attribution corrected — round 10 of wave anchor #24579. The line said the walled boot refusal was 18.0's ('the 18.0 refusal') and that 18.x step 4 (#24580) deleted it, so a runner on 18.0.0 could not tell whether its release boots the walled posture. Under ruling record 6282882793 on the v18 epic (items 1 and 2), 18.0.0 is published only after step 4 has merged, and a walled posture boots on it. Of the published @objectstack/cli prereleases only 18.0.0-next.5 carries serve.ts walledPostureRefusal: next.0 to next.3 predate it, measured in each npm tarball and at each version tag, and no next.4 was published. Only that attribution changed: no clause, step or other knownGap moved", "ref": "#24788" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "fixtures.requires[2] names the recipe for its organization-scoped manage_metadata grant, and knownGaps[0]'s prerequisite sentence says os dev — QA run #24783 (findings F7 and F2), round 11 of wave anchor #24579. No HTTP door placed the grant on the walled boot: the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, and the owner's own insert by ADR-0090 D12 condition (c), manage_metadata being scope 'platform'. The recipe is the dogfood harness's system-context engine insert (wall-environment-metadata-refusal.dogfood.test.ts, its boot helper). A bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md), so the refusal sentence now names os dev. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round11" } ] }, diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 53363b11ad5..495035cf069 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -1666,7 +1666,7 @@ "title": "POST /actions/_activation/:object/:action — the activation door's ordered arms: authority before body and lookup, the exact 400 shapes, 404 undeclared, 409 ambiguity, 501/503 honesty, and the reserved segment surviving the real mount", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "api", "personas": [ @@ -1683,7 +1683,7 @@ "knownGaps": [ "The AMBIGUITY arm (409 RESOURCE_CONFLICT from refuseAmbiguousActionActivation — two objects declaring one action name) has NO stock fixture: every showcase action name is unique across both action files (verified — the machine names are all showcase_-prefixed and declared once), so the live leg needs an authored scratch package declaring the SAME action name on two objects. Do not fake it; the arm is pinned at unit level (action-activation-posture-gate.test.ts: 409 RESOURCE_CONFLICT naming both owners, and setActionActive NEVER called — no row on refusal), and a run without the scratch package scores the clause from that pin, recorded as such.", "The 501 arm (engine without setActionActive) and the 503 arm (attached store that throws / no ledger attached) are unreachable on a stock boot — the stock engine implements the projection and the plugin attaches the store. Both are pinned at unit level (action-activation-posture-gate.test.ts: 501 naming ADR-0126 §8; 503 SERVICE_UNAVAILABLE rather than a 200 for a write that did not persist). #12359 measured the 503 live once (actions-without-automation composition) and the registration MOVE closed it — packaged-activation-ledger-reach.dogfood.test.ts keeps that composition green.", - "The group/isolated POSTURE legs of the §5 operator gate need an enterprise tenancy runtime no example app composes — the gate itself (both tiers, all postures) is access-security territory (activation-write-operator-gate); this item asserts only the ORDER (authority before body/lookup), which the stock single-posture boot can prove through the manage_metadata tier.", + "The group/isolated POSTURE legs of the §5 operator gate run on a walled boot provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own' (a scratch app declaring the open @objectstack/organizations, which no example app declares). The gate itself (both tiers, all postures) is access-security territory (activation-write-operator-gate); this item asserts only the ORDER, authority before body and lookup (packages/runtime/src/domains/actions.ts#handleActionActivationWrite), which the stock single-posture boot proves through the manage_metadata tier and a walled boot through the operator tier: on both postures QA run #24783's org admin holding manage_metadata got the posture/operator 403 for the mis-bodied, undeclared probe, never the 400 or the 404.", "acceptance[4]'s metadata-plane 503 arm — the declaration lookup reports `degraded` (packages/runtime/src/domains/actions.ts, the 'Cannot verify the declaration … the metadata plane is unavailable' refusal) — is unreachable on a stock boot and pinned by no file in automated.ref: action-activation-posture-gate.test.ts's 503 is the store that cannot persist the row, a different arm, and http-dispatcher.actions-type-dispatch.test.ts '503s rather than running ungated when the metadata plane cannot answer' pins the DISPATCH door's use of the same lookup, not this door. Record that arm as not covered — no live door, no pin — never as a pass carried by the 404" ] }, @@ -1790,6 +1790,12 @@ "date": "2026-10-19", "change": "knownGaps and acceptance[4].verify record the one arm nothing covers — checklist-accuracy finding of the 18.0 QA sweep (QA run #24541, api-backend.action-activation-door-contract acceptance[4]; wave anchor #24579 §3): the metadata-plane-degraded 503 arm is named in the clause but is neither stock-reachable nor pinned by any file in automated.ref. Re-derived at base 9a7838edf5: the arm is actions.ts's declaration.degraded branch; action-activation-posture-gate.test.ts pins the non-durable store 503 instead, and the metadata-plane 503 pin that exists (http-dispatcher.actions-type-dispatch.test.ts) drives the dispatch door. No clause changed", "ref": "#24649" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "fixtures.knownGaps[2] re-pointed — QA run #24783 (finding F5), round 11 of wave anchor #24579. 'Needs an enterprise tenancy runtime no example app composes' is stale since 18.x step 4 (#24580): a scratch app declaring the open @objectstack/organizations composes the walled posture (RUNNER.md 'A walled boot is a fixture of its own'), and QA run #24783 measured the authority-first order on both walled postures. The line now points at that entry and names the handler whose order it asserts. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round11" } ] }, diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index 180a63decc6..3ed1a3df486 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -2005,15 +2005,15 @@ "title": "Workspace switcher: org chrome follows posture × membership count (nothing under single; read-only indicator at one membership; switcher at two), and a switch is a full-document reload after which every tenant-scoped read answers from the new org", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", - "personas": ["a member with one org membership", "the same member after gaining a second membership", "admin (provisions the second org)"], + "personas": ["a member with one org membership", "the same member after gaining a second membership", "admin, the walled boot's platform operator (OS_PLATFORM_OWNER_EMAIL): mints the member with POST /api/v1/auth/admin/create-user (RUNNER.md 'A walled boot is a fixture of its own'). It does not provision the second org: the member creates that itself (fixtures.requires[1])"], "fixtures": { "app": "showcase", "requires": [ - "a WALL-ENFORCING tenancy posture boot: OS_TENANCY_POSTURE=group (or isolated) — the stock default resolves to `single` (resolveTenancyPosture, packages/types/src/env.ts), where multiOrgEnabled is false and NO org chrome may render at all (postureEnforcesWall drives both the flag and the UI predicate — auth-manager.ts). A walled posture REFUSES TO BOOT the stock showcase without three more things, measured by QA run #23166 from a scratch copy: (1) `@objectstack/organizations` declared in the host app's package.json — the multi-org runtime a walled posture needs (ADR-0093 D5); (2) OS_PLATFORM_OWNER_EMAIL, the declared platform owner (set it to the seeded admin's address, whose seed stamps it verified so it can be elevated); (3) a declared membership policy, OS_AUTH_MEMBERSHIP_POLICY (packages/plugins/organizations/src/membership-policy-gate.ts#MEMBERSHIP_POLICY_ERROR_CODE — WALLED_MEMBERSHIP_POLICY_UNDECLARED). Boot it from a scratch copy, never the tree", - "a second organization + membership for the test user — runtime-creatable on the posture boot via the better-auth org endpoints (POST /api/v1/auth/organization/create, then the platform-admin-gated POST /api/v1/auth/organization/add-member — auth-route-ledger.ts organization family); the org-create gate follows the SAME posture derivation as the advertised flag, so it admits on this boot. Under OS_AUTH_MEMBERSHIP_POLICY=invite-only the invited test user must also ACCEPT the invitation (POST /api/v1/auth/organization/accept-invitation) before the membership exists" + "a WALL-ENFORCING tenancy posture boot: OS_TENANCY_POSTURE=group (or isolated) — the stock default resolves to `single` (resolveTenancyPosture, packages/types/src/env.ts), where multiOrgEnabled is false and NO org chrome may render at all (postureEnforcesWall drives both the flag and the UI predicate — auth-manager.ts). Provision and boot it per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own' (a full-copy scratch app declaring the multi-org runtime, OS_PLATFORM_OWNER_EMAIL and a declared OS_AUTH_MEMBERSHIP_POLICY, booted through os dev), never from the tree. Set OS_PLATFORM_OWNER_EMAIL to the seeded admin's address, whose seed stamps it verified so it can be elevated", + "a second organization + membership for the test user, which the user creates itself on the posture boot: POST /api/v1/auth/organization/create makes its caller the new organization's owner, so a user who creates a second organization holds two memberships (QA run #24783: the member created a second organization and switched to it with set-active on both postures). The org-create gate follows the SAME posture derivation as the advertised flag, so it admits on this boot. The platform-admin-gated POST /api/v1/auth/organization/add-member (auth-route-ledger.ts organization family) is not needed; a membership reached through an invitation instead exists only once the invited user ACCEPTS it (POST /api/v1/auth/organization/accept-invitation)" ], "knownGaps": [ "not runnable on the stock boot: `single` posture renders no switcher and no indicator BY DESIGN (that absence is itself this item's clause 1, runnable on stock) — every other clause needs the OS_TENANCY_POSTURE boot and is blocked(fixture) without it" @@ -2088,7 +2088,13 @@ { "revision": 1, "date": "2026-08-30", "change": "authored in the 2026-08-30 coverage sweep (angle 1): the workspace-switch surface had no item. CORRECTED against source from the register's 'single membership → indicator' hypothesis: the indicator is posture-gated (postureHasOrgWall — group/isolated only), so the stock `single` boot renders NO org chrome at all and the whole matrix needs an OS_TENANCY_POSTURE boot; encoded the three-cell affordance matrix, the deliberate full-document reload, the #4486 cache-drop read-follow proof, group-posture labeling, and the forged-switch refusal", "ref": "#sweep-2026-08-30" }, { "revision": 2, "date": "2026-10-05", "change": "clause 6 and step 8 re-pointed (assertion defect, #21784 VF4). The switch fails closed, and the defect is not of the authorization class. better-auth 1.7.3's setActiveOrganization (plugins/organization/routes/crud-org) calls adapter.setActiveOrganization(token, null) before it throws 403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, and plugin-auth mounts the route unwrapped (auth-route-ledger 'sdk'). So the active org becomes none, never the foreign org, and 'unchanged' was the wrong assertion. The verifier's wording is used. The verifier's optional follow-up, wrapping the vendor call to keep the previous org, would be a product enhancement, not this item's concern", "ref": "#21797" }, { "revision": 3, "date": "2026-10-13", "change": "fixtures.requires[0] and [1] name the walled-posture boot prerequisites (fixture gap, #23166), and steps[6] and acceptance[3] re-ground the meta half (stale spec, #23166 C3b); both parked on #23121, carried by #23169. QA run #23166 found a walled posture refuses to boot the stock showcase without @objectstack/organizations in the host package.json (ADR-0093 D5), OS_PLATFORM_OWNER_EMAIL and OS_AUTH_MEMBERSHIP_POLICY, and that under invite-only the invitee must accept the invitation. It skipped C3b because its premise is retired: metadata writes are deployment-level (ADR-0131 D6/D7), so no metadata id exists in one org only. The clause keeps the data sentinel and now expects the /meta answer to be identical across the switch", "ref": "#23169" }, - { "revision": 4, "date": "2026-10-17", "change": "steps[6] and acceptance[3] (clause, verify) split by posture (checklist-accuracy finding of the identity-auth slice report on #23878 (6216631966)). The sentinel-absent check holds only under `isolated`: under `group` the active org bounds writes, not reads, so a row that exists only in the old org stays readable after the switch, and the check would FAIL a correct product. Under group the clause now proves the switch on the write side — a post-switch create lands in the new org. Not loosened: under isolated the old-org row must still be absent", "ref": "#24036" } + { "revision": 4, "date": "2026-10-17", "change": "steps[6] and acceptance[3] (clause, verify) split by posture (checklist-accuracy finding of the identity-auth slice report on #23878 (6216631966)). The sentinel-absent check holds only under `isolated`: under `group` the active org bounds writes, not reads, so a row that exists only in the old org stays readable after the switch, and the check would FAIL a correct product. Under group the clause now proves the switch on the write side — a post-switch create lands in the new org. Not loosened: under isolated the old-org row must still be absent", "ref": "#24036" }, + { + "revision": 5, + "date": "2026-10-20", + "change": "fixtures.requires[0]–[1] and personas[2] re-pointed — QA run #24783 (finding F8), round 11 of wave anchor #24579. requires[0]'s 'REFUSES TO BOOT … measured by QA run #23166' predates 18.x step 4 (#24580); the provisioning now points at RUNNER.md's walled-boot entry, keeping only the seeded-admin owner-email hint. requires[1] and personas[2]: the user gets its second membership by creating a second organization itself (QA run #24783, both postures), so the platform-admin add-member is not needed and the admin only mints the member. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round11" + } ] }, { diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index 1145024ab6a..9a15e0c2c0a 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -534,13 +534,13 @@ "title": "Outbound webhooks are read from the registry (spec object→object_name, no sys_webhook row) and switched through the activation ledger, fire per trigger variant through the sys_http_delivery outbox with HMAC + timeout honored, redeliver terminal rows tenant-scoped via POST /api/v1/webhooks/redeliver, and reject retired trigger kinds", "since": "v15", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "mixed", "fixtures": { "app": "showcase", "requires": [ - "a reachable webhook receiver (local echo server on the run's own port range) and a SCRATCH webhook authored INSIDE the showcase organization that points at it — Setup → Integrations → Webhooks → New as the signed-in admin, or POST /api/v1/data/sys_webhook under that session, so the row carries the organization. ⚠ The shipped showcase_task_changed cannot carry the per-record or bulk delivery legs: it is a declared webhook, read-only, so its url stays the placeholder hooks.example and cannot be pointed at the receiver. Once switched on it DOES deliver under `single` — a declared webhook is deployment-wide there and each delivery is stamped with the event's organization (packages/plugins/plugin-webhooks/src/auto-enqueuer.ts, AutoEnqueuer.admitsOrganization, CachedSubscription.deploymentWide) — and under a walled posture it would be refused as belonging to no organization", + "a reachable webhook receiver (local echo server on the run's own port range) and a SCRATCH webhook authored INSIDE the showcase organization that points at it — Setup → Integrations → Webhooks → New as the signed-in admin, or POST /api/v1/data/sys_webhook under that session, so the row carries the organization. ⚠ The shipped showcase_task_changed cannot carry the per-record or bulk delivery legs: it is a declared webhook, read-only, so its url stays the placeholder hooks.example and cannot be pointed at the receiver. Once switched on it DOES deliver under `single` — a declared webhook is deployment-wide there and each delivery is stamped with the event's organization (packages/plugins/plugin-webhooks/src/auto-enqueuer.ts, AutoEnqueuer.admitsOrganization, CachedSubscription.deploymentWide) — and the same holds under `isolated` and `group` (ADR-0131 D2/D9, ruling A: CachedSubscription.deploymentWide is set on every declared webhook). Switched on by the platform operator, since under a wall the activation door is operator-only, it delivers one sys_http_delivery row per event stamped with that event's organization, and each organization reads only its own rows: packages/qa/dogfood/test/wall-declared-webhook-deployment-wide.dogfood.test.ts pins it in its '[ruling A] isolated|group: a declared webhook is deployment-wide' describe, run under both postures, and QA run #24783 measured it live. Authoring the scratch webhook over POST /api/v1/data/sys_webhook needs `definition_json` (required: the serialised Webhook envelope of @objectstack/spec/automation/webhook) and a lower-case `method` such as `post`, because Field.select lower-cases the select's option values (packages/plugins/plugin-webhooks/src/sys-webhook.object.ts)", "the shipped fixture: showcase_task_changed (object showcase_task, triggers create/update/delete, isActive:false ON PURPOSE — switching it on through the activation door is part of the test, examples/app-showcase/src/automation/webhooks/index.ts)", "a predicate multi-write path for the bulk variants (update/delete with multi:true on showcase_task) — NOT reachable over REST, see the knownGap below: author a flow with an `update_record` / `delete_record` node carrying multi:true and fire it through the api trigger" ], @@ -683,6 +683,12 @@ "date": "2026-10-19", "change": "ADR-0131 D2/D6: the declared-webhook boot seeder retired, so a declared webhook is read from the registry and writes no sys_webhook row, and is switched through POST /api/v1/security/_activation/webhook/:name (regime C). steps[0], steps[1] and the reboot step, acceptance[0] and acceptance[6], fixtures.requires[0]/[1] and the source list now say so; the delivery, bulk, redeliver and retired-trigger clauses are unchanged", "ref": "#24529" + }, + { + "revision": 9, + "date": "2026-10-20", + "change": "fixtures.requires[0]'s walled sentence and scratch-webhook recipe corrected — QA run #24783 (finding F4), round 11 of wave anchor #24579. 'Under a walled posture it would be refused as belonging to no organization' predates ADR-0131 ruling A: a declared webhook is deployment-wide under every posture (auto-enqueuer.ts admitsOrganization answers true for CachedSubscription.deploymentWide), delivers one row per event stamped with that event's organization, and is switched on by the operator under a wall, as wall-declared-webhook-deployment-wide.dogfood.test.ts pins and QA run #24783 measured on both postures. The scratch sys_webhook recipe now names its required definition_json and the lower-case method values sys-webhook.object.ts declares. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round11" } ], "enumSource": { diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 40479346f8d..97de071fdde 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -3217,7 +3217,7 @@ "title": "Settings on v18: a global-scope key is stored in sys_platform_setting (readable through /data only with manage_platform_settings), and under a walled posture one organization's tenant save or lock does not change or lock another's, with a data-API read of sys_setting filtered to readable namespaces", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": [ @@ -3230,16 +3230,17 @@ "requires": [ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", "a second boot with OS_TENANCY_POSTURE=isolated (walled posture) on its own port and file DB — the stock single posture leaves every walled clause vacuous, so confirm the posture from the boot log before scoring", - "on the walled boot: two organizations A and B with an admin each" + "on the walled boot: two organizations A and B with an admin each, and A's tenant row of the probed key locked by a system write, because no HTTP door sets that lock (knownGaps[1])" ], "knownGaps": [ - "acceptance[2] and acceptance[3] are walled legs, and fixtures.requires[1]–[2], personas[2] and steps[2]–[3] serve only them. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses the boot naming itself. On that boot organizations A and B are created through POST /api/v1/auth/organization/create (RUNNER.md 'A stock `single` boot holds exactly one organization, and no door mints a second; a walled boot mints them through the auth API'). A run that does not provision them records both clauses blocked(fixture) naming those entries, never fail. Their mechanism is scored through acceptance[0]: settings-organization-isolation.pin.test.ts drives two organizations, the lock and the organization-less refusal under a walled posture in-process, and asserts that `single` does not refuse. acceptance[0] and acceptance[1] run as written." + "acceptance[2] and acceptance[3] are walled legs, and fixtures.requires[1]–[2], personas[2] and steps[2]–[3] serve only them. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses an os dev boot naming itself, while a bare os serve in production mode judges none of them until OS_AUTH_SECRET is set (that entry). On that boot organizations A and B are created through POST /api/v1/auth/organization/create (RUNNER.md 'A stock `single` boot holds exactly one organization, and no door mints a second; a walled boot mints them through the auth API'). A run that does not provision them records both clauses blocked(fixture) naming those entries, never fail. Their mechanism is scored through acceptance[0]: settings-organization-isolation.pin.test.ts drives two organizations, the lock and the organization-less refusal under a walled posture in-process, and asserts that `single` does not refuse. acceptance[0] and acceptance[1] run as written.", + "No HTTP door sets a tenant-row lock. sys_setting declares apiMethods ['get', 'list'] (packages/platform-objects/src/system/sys-setting.object.ts), so an update over /api/v1/data answers 405 OBJECT_API_METHOD_NOT_ALLOWED, and PUT /api/settings/NAMESPACE carries values only: packages/services/service-settings/src/settings-service.ts#setMany takes a key-to-value patch, and that service reads a row's lock but never writes one. steps[2]'s lock is therefore a fixture prerequisite (fixtures.requires[2]): stamp locked = 1 on A's tenant sys_setting row with a system write on the run's own scratch DB, as QA run #24783 did (never on a shared DB), and record it in the run record as a fixture write made outside HTTP. settings-organization-isolation.pin.test.ts seeds its lock the same way, as a stored row. Whether a tenant lock should have an HTTP door is unruled; QA run #24783 raised it (finding F6) for wave anchor #24579" ] }, "steps": [ "as platform admin write a global-scope key through PUT /api/settings/NAMESPACE; GET /api/v1/data/sys_platform_setting and /sys_setting?filter scope=global", "as the member GET /api/v1/data/sys_platform_setting", - "walled: as A's admin save a tenant key and lock it; as B's admin read and write the same key", + "walled: as A's admin save a tenant key (PUT /api/settings/NAMESPACE); lock A's row with the fixture write of fixtures.requires[2] (knownGaps[1]) and re-read the key as A (locked: true); as B's admin read and write the same key", "walled: write a tenant key with no organization in scope" ], "acceptance": [ @@ -3305,6 +3306,12 @@ "date": "2026-10-20", "change": "fixtures.knownGaps[0]'s release attribution corrected — round 10 of wave anchor #24579. The line said the walled boot refusal was 18.0's ('the 18.0 refusal') and that 18.x step 4 (#24580) deleted it, so a runner on 18.0.0 could not tell whether its release boots the walled posture. Under ruling record 6282882793 on the v18 epic (items 1 and 2), 18.0.0 is published only after step 4 has merged, and a walled posture boots on it. Of the published @objectstack/cli prereleases only 18.0.0-next.5 carries serve.ts walledPostureRefusal: next.0 to next.3 predate it, measured in each npm tarball and at each version tag, and no next.4 was published. Only that attribution changed: no clause, step or other knownGap moved", "ref": "#24788" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[2]'s tenant lock made a named fixture prerequisite (fixtures.requires[2], new knownGaps[1]), and knownGaps[0]'s prerequisite sentence says os dev — QA run #24783 (findings F6 and F2), round 11 of wave anchor #24579. No HTTP door sets a tenant-row lock: sys_setting answers update 405 (apiMethods get/list) and the settings PUT writes values only, so the run stamped the lock with a system write on its scratch DB. A bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md), so the refusal sentence now names os dev. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round11" } ] }, From fdf44fe5e4013c945cc5376360d09ae1277c0444 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 06:58:13 +0000 Subject: [PATCH 3/3] docs(qa): round 11's history rows name their pull request Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/access-security.json | 8 ++++---- docs/qa/platform-checklist/areas/api-backend.json | 2 +- docs/qa/platform-checklist/areas/identity-auth.json | 2 +- docs/qa/platform-checklist/areas/integration-system.json | 2 +- docs/qa/platform-checklist/areas/platform-core.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 5a2c2f5c6fb..af6e592d327 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -931,7 +931,7 @@ "revision": 4, "date": "2026-10-20", "change": "automated dropped and fixtures.knownGaps[0] added, under round 6's theme-B rule (drop `automated` where no pin asserts a full clause, with a knownGaps line) — round 11 of wave anchor #24579, whose round-3 summary named this nominally Tier 1 item. Its revision-3 automated.ref already said the three dogfood pins cover no clause. Re-read at 46bb5e3e54: they drive business-unit depth on showcase notes and never the manager / executive / ops personas or showcase_inquiry. The item is now Tier 2, every clause driven by hand as revision 3 already prescribed. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round11" + "ref": "#24801" } ] }, @@ -2485,7 +2485,7 @@ "revision": 6, "date": "2026-10-20", "change": "producers re-measured and acceptance[0] aligned with its governing text — QA run #24783 (finding F3), round 11 of wave anchor #24579. personas[0]–[1], fixtures.requires[1] and knownGaps[0]–[1] now name the two durable producers on a walled boot (a user the operator creates under invite-only, membershipCreated:false; a member that sets its active organization to null). The signup race is closed by ADR-0123 D3's session-seam settle, and the removed-member path revokes or re-points the session (QA run #24783 measured 401). acceptance[0]'s clause and verify scope 'resolve to nothing' to a caller with no organization scope, which ADR-0123 D2 defines under `group` as an empty membership set, ADR-0105 D2 letting a `group` member's reads span its memberships. acceptance[4] is unchanged: ADR-0123 states no answer for a member-less user's NULL-tenanted audit rows, so knownGaps[3] names the question. knownGaps[4] names #24784, the `group` write 500. knownGaps[2]: the prerequisite sentence holds through os dev only (a bare production os serve needs OS_AUTH_SECRET first, RUNNER.md), and the blocked marker's reason is restated (every clause is a walled leg) because a producer is no longer missing; the marker itself is unchanged", - "ref": "claude/issue-24579-v18-sweep-round11" + "ref": "#24801" } ] }, @@ -2640,7 +2640,7 @@ "revision": 8, "date": "2026-10-20", "change": "fixtures.requires[3] names the recipe for the walled legs' org-owner manage_metadata grant, and knownGaps[0]'s prerequisite sentence says os dev — QA run #24783 (findings F7 and F2), round 11 of wave anchor #24579. No HTTP door placed the grant on the walled boot: the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, and the owner's own insert by ADR-0090 D12 condition (c), manage_metadata being scope 'platform'. The recipe is the dogfood harness's system-context engine insert (wall-environment-metadata-refusal.dogfood.test.ts, its boot helper). A bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md), so the refusal sentence now names os dev. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round11" + "ref": "#24801" } ] }, @@ -3524,7 +3524,7 @@ "revision": 5, "date": "2026-10-20", "change": "fixtures.requires[2] names the recipe for its organization-scoped manage_metadata grant, and knownGaps[0]'s prerequisite sentence says os dev — QA run #24783 (findings F7 and F2), round 11 of wave anchor #24579. No HTTP door placed the grant on the walled boot: the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, and the owner's own insert by ADR-0090 D12 condition (c), manage_metadata being scope 'platform'. The recipe is the dogfood harness's system-context engine insert (wall-environment-metadata-refusal.dogfood.test.ts, its boot helper). A bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md), so the refusal sentence now names os dev. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round11" + "ref": "#24801" } ] }, diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 495035cf069..ed165fe6372 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -1795,7 +1795,7 @@ "revision": 4, "date": "2026-10-20", "change": "fixtures.knownGaps[2] re-pointed — QA run #24783 (finding F5), round 11 of wave anchor #24579. 'Needs an enterprise tenancy runtime no example app composes' is stale since 18.x step 4 (#24580): a scratch app declaring the open @objectstack/organizations composes the walled posture (RUNNER.md 'A walled boot is a fixture of its own'), and QA run #24783 measured the authority-first order on both walled postures. The line now points at that entry and names the handler whose order it asserts. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round11" + "ref": "#24801" } ] }, diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index 3ed1a3df486..3525723491b 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -2093,7 +2093,7 @@ "revision": 5, "date": "2026-10-20", "change": "fixtures.requires[0]–[1] and personas[2] re-pointed — QA run #24783 (finding F8), round 11 of wave anchor #24579. requires[0]'s 'REFUSES TO BOOT … measured by QA run #23166' predates 18.x step 4 (#24580); the provisioning now points at RUNNER.md's walled-boot entry, keeping only the seeded-admin owner-email hint. requires[1] and personas[2]: the user gets its second membership by creating a second organization itself (QA run #24783, both postures), so the platform-admin add-member is not needed and the admin only mints the member. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round11" + "ref": "#24801" } ] }, diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index 9a15e0c2c0a..16adba5f06b 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -688,7 +688,7 @@ "revision": 9, "date": "2026-10-20", "change": "fixtures.requires[0]'s walled sentence and scratch-webhook recipe corrected — QA run #24783 (finding F4), round 11 of wave anchor #24579. 'Under a walled posture it would be refused as belonging to no organization' predates ADR-0131 ruling A: a declared webhook is deployment-wide under every posture (auto-enqueuer.ts admitsOrganization answers true for CachedSubscription.deploymentWide), delivers one row per event stamped with that event's organization, and is switched on by the operator under a wall, as wall-declared-webhook-deployment-wide.dogfood.test.ts pins and QA run #24783 measured on both postures. The scratch sys_webhook recipe now names its required definition_json and the lower-case method values sys-webhook.object.ts declares. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round11" + "ref": "#24801" } ], "enumSource": { diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 97de071fdde..52474c93c6b 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -3311,7 +3311,7 @@ "revision": 5, "date": "2026-10-20", "change": "steps[2]'s tenant lock made a named fixture prerequisite (fixtures.requires[2], new knownGaps[1]), and knownGaps[0]'s prerequisite sentence says os dev — QA run #24783 (findings F6 and F2), round 11 of wave anchor #24579. No HTTP door sets a tenant-row lock: sys_setting answers update 405 (apiMethods get/list) and the settings PUT writes values only, so the run stamped the lock with a system write on its scratch DB. A bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md), so the refusal sentence now names os dev. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round11" + "ref": "#24801" } ] },