Skip to content

Commit 79222a3

Browse files
committed
docs(qa): RUNNER walled-boot entry — the serve-mode auth secret, the full-copy scratch app, the admin persona door; the setsid stop recipe
Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude <noreply@anthropic.com>
1 parent 46bb5e3 commit 79222a3

1 file changed

Lines changed: 73 additions & 4 deletions

File tree

‎docs/qa/platform-checklist/RUNNER.md‎

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,25 @@ contradicts it, and correct it here when it does.
302302
the real port). This is "zero hits needs a positive control" applied to one tool: a
303303
negative from a command that never ran is not evidence.
304304

305+
- **A SIGTERM to the `pnpm` wrapper does not stop the server under it — start a
306+
backgrounded boot with `setsid` and stop its process group.** The canonical boot (in the
307+
`--no-ui` entry below) is `pnpm -C … exec objectstack dev …`: pnpm spawns `os dev`, which
308+
spawns `serve --dev` (`packages/cli/src/commands/dev.ts`). `os dev` forwards a SIGINT or
309+
SIGTERM sent to its own pid to that child (its `process.on('SIGTERM', …)` handler calls
310+
`beginShutdown()` of `ServeRestartCoordinator`, `packages/cli/src/utils/dev-restart.ts`),
311+
but nothing forwards one sent to pnpm, so `kill` on the pnpm pid leaves the `serve`
312+
grandchild listening. Both runners of the 18.0 sweep's round 3 met it (wave anchor
313+
#24579 §6). Measured at
314+
`46bb5e3e54` with a stand-in tree, `pnpm -C examples/app-showcase exec node parent.js`
315+
where `parent.js` forwards SIGTERM to an HTTP child as `os dev` does: two seconds after a
316+
SIGTERM to the pnpm pid the child still answered 200, and a SIGTERM to the process group
317+
stopped all four processes. ⚠️ `$!` is not the group id: a backgrounded `setsid` forks
318+
when the shell made it a group leader, so `$!` names a process that has already exited.
319+
**So:** `setsid pnpm -C … exec objectstack dev … > LOG 2>&1 &`; once the port answers,
320+
read the group from the listener, `PGID=$(ps -o pgid= -p "$(lsof -ti tcp:PORT)" | tr -d ' ')`;
321+
stop it with `kill -TERM -- -$PGID`, and confirm `lsof -ti tcp:PORT` then prints nothing.
322+
⛔ Never stop a server by name (`pkill`), and never a group you did not start.
323+
305324
- **Console session auth is a bearer token in `localStorage`, and a form sign-in ALSO
306325
sets a cookie — you need both halves.** The console's auth client stores the session
307326
under `auth-session-token` (objectui `packages/auth/src/createAuthClient.ts`:
@@ -466,17 +485,67 @@ contradicts it, and correct it here when it does.
466485
So on 18.0.0
467486
`OS_TENANCY_POSTURE=group` or `isolated` (or the legacy `OS_MULTI_ORG_ENABLED=true`, which
468487
`resolveTenancyPosture()` in `packages/types/src/env.ts` reads as `isolated`) boots through
469-
`os serve`, and so `os dev` and `os start`. Each prerequisite below refuses the boot with a
470-
FATAL naming itself when absent, so a refusal tells the runner what is missing, never that
471-
the posture is unsupported:
488+
`os serve`, and so `os dev` and `os start`. Through `os dev`, each prerequisite below
489+
refuses the boot with a FATAL naming itself when absent (QA run #24783 measured all three),
490+
so a refusal tells the runner what is missing, never that the posture is unsupported:
472491
1. the multi-org runtime **declared by the app**: `@objectstack/organizations` in the app's
473492
own `package.json` and installed there. Reachable only through `NODE_PATH` or a hoisted
474493
store is refused on purpose (ADR-0093 D5). No example app declares it, so a walled run
475-
uses a scratch app that does;
494+
uses a scratch app that does: a **full copy** of `examples/app-showcase`, `src/`
495+
included (without `node_modules`, `.objectstack` or `dist`), whose own `package.json`
496+
carries every showcase dependency as a `link:` to its workspace package plus
497+
`"@objectstack/organizations": "link:<abs>/packages/plugins/organizations"`, installed
498+
from the copy with `pnpm install --ignore-workspace` (QA run #24783). A scratch config
499+
that only re-exports the showcase's `objectstack.config.ts` does not boot: a package-file
500+
reference resolves against the root being served, and the showcase's OpenAPI connector
501+
names its document that way (`spec: './src/system/connectors/status-openapi.json'` in
502+
`examples/app-showcase/src/system/connectors/index.ts`, read relative to the package
503+
root at materialization, where a missing document fails the boot);
476504
2. `OS_PLATFORM_OWNER_EMAIL`, the deployment's platform administrators: first-registrant
477505
promotion is off under a wall;
478506
3. a declared membership policy, `OS_AUTH_MEMBERSHIP_POLICY` (for example `invite-only`).
479507

508+
⚠️ **A bare `os serve` in production mode judges none of the three until it has an auth
509+
secret.** Without `--dev`, and with `NODE_ENV` anything but `development`, `serve` composes
510+
the auth family only when `OS_AUTH_SECRET` (or a legacy spelling) resolves —
511+
`resolvePlatformAuthComposition()` in `packages/core/src/stack-auth.ts`, reason
512+
`no-secret` — and all three checks sit behind it: the multi-org runtime is loaded inside
513+
`serve.ts`'s auth-composition branch, the owner-email check runs in `AuthPlugin`'s `init()`
514+
(`packages/plugins/plugin-auth/src/auth-plugin.ts`), and the membership-policy gate skips a
515+
kernel with no `auth` service (`assertWalledMembershipPolicyDeclared()` in
516+
`packages/plugins/organizations/src/membership-policy-gate.ts`). With the secret unset the
517+
boot prints `⚠ AuthPlugin skipped — set OS_AUTH_SECRET …` and comes up
518+
`⚠ Server is ready — DEGRADED: missing core services: auth` (`printServerReady()` in
519+
`packages/cli/src/utils/format.ts`), not a FATAL: QA run #24783 measured it with
520+
`os serve --no-ui` from the scratch app, once with prerequisite 2 absent and once with
521+
prerequisite 3 absent. A production `serve` also needs a stable encryption key,
522+
`OS_SECRET_KEY` or a key file an earlier boot persisted; without one `LocalCryptoProvider`
523+
refuses the boot naming `OS_SECRET_KEY`
524+
(`packages/services/service-settings/src/local-crypto-provider.ts`). `os dev` needs
525+
neither: it spawns `serve --dev`, whose development boot falls back to
526+
`DEV_AUTH_SECRET_FALLBACK` (`resolveAuthSecret()`, same `stack-auth.ts`) and persists a key.
527+
`os start` hands its `serve` child an auth secret (its `--auth-secret` flag, the
528+
environment, or the `auth-secret` file it persists in its home directory) and opts the
529+
provider into a persisted key (`OS_CRYPTO_AUTOKEY`), in `packages/cli/src/commands/start.ts`
530+
— read from the code, not booted. **So:** boot a walled run through `os dev`, or give a bare
531+
`serve` `OS_AUTH_SECRET` and a stable key first. ⛔ Never read a DEGRADED walled boot as
532+
"the prerequisites are met", and never score a walled clause on it.
533+
534+
**Walled personas come from the admin door.** The walled boot keeps self-registration
535+
closed: the showcase declares no `auth.audience`, so `POST /api/v1/auth/sign-up/email`
536+
answers **`403 SELF_REGISTRATION_CLOSED`** exactly as on a stock boot
537+
(`decideAudienceAdmission()` in `packages/plugins/plugin-auth/src/audience-posture.ts`; the
538+
entry "A stock showcase boot refuses self-registration" above). QA run #24783 minted every
539+
walled persona as the operator (the seeded admin, the address `OS_PLATFORM_OWNER_EMAIL`
540+
names) with `POST /api/v1/auth/admin/create-user` (`runAdminCreateUser()` in
541+
`packages/plugins/plugin-auth/src/admin-user-endpoints.ts`), then had each persona
542+
`POST /api/v1/auth/change-password`. Under `invite-only` each lands with
543+
`membershipCreated: false` and in no organization (the reconciler's `policy-skip`,
544+
`reconcileMembership()` in `packages/plugins/plugin-auth/src/reconcile-membership.ts`); a
545+
persona that needs an organization creates its own with
546+
`POST /api/v1/auth/organization/create` and switches to it with `set-active` (the next
547+
entry).
548+
480549
Measured at `45898dbf6`: a scratch app declaring the real runtime, booted by the built CLI
481550
under `isolated` with the other two set, mounted `Organizations`, answered
482551
`GET /api/v1/discovery` 200 and logged no system-write refusal. The seven walled

0 commit comments

Comments
 (0)