Repository navigation
Commit 24d622b
feat(spec,cli): let an app contribute its own first-run credentials to the boot banner (
Fixes #17556
Clause-②: yes (widening)
Suggestion 1 of #17081, filed onto its own card so PR #17555's `Closes`
would not take it
down with the parent. #17555 took the platform's half — the `🔑 Dev
admin` line now says
what that account will and will not see. This is the half `packages/cli`
structurally
could not write: the platform can describe the account it seeds, and it
cannot know that an
application's `Hiring` group needs a position or which of five personas
a demo should open
with. The application knows, and now has a channel to say so.
## What an operator sees
Nothing changes unless the application declares something, and nothing
changes outside a
development boot. With both keys declared:
```
🔑 Dev admin: admin@objectos.ai / admin123
seeded on empty DB · dev only — do not use in production
platform admin — Setup, Studio and every record, but NO app-declared capability, so
an app that gates navigation on requiredPermissions may show it an empty menu; grant
it a permission set under Setup → Users, or sign in as an account your app seeds
👥 App logins: 2 declared by this app
Hiring admin — admin@quillstone.example / demo1234
Job seeker — candidate01@mail.example / demo1234
declared in this app's `devLogins` · dev only — the platform seeded none of them
💡 App hint: run `pnpm seed:demo` first, then sign in as the Hiring admin
```
## The accept-set delta, stated so a contract review can check it
The top-level stack door has been strict since #8687, so both spellings
were an
`unrecognized_keys` refusal before this change. What is writable after
that was not
writable before, and nothing else:
| surface | before | after |
|:--|:--|:--|
| `ObjectStackDefinitionSchema.devHint` | refused (`unrecognized_keys`,
path `[]`) | `string`, optional |
| `ObjectStackDefinitionSchema.devLogins` | refused
(`unrecognized_keys`, path `[]`) | array of `DevLogin`, optional |
| `DevLogin.email` | did not exist | `string`, required, email-validated
|
| `DevLogin.password` | did not exist | `string`, optional |
| `DevLogin.label` | did not exist | `string`, optional |
| any other key inside a `devLogins` entry | did not exist | refused —
the entry is `strictObject` from birth |
| any other new top-level key | refused | still refused (pinned as the
dark control) |
Nothing is renamed, nothing is retired, no value that parsed before is
refused now, and
the delta is recorded in two tracked artifacts a review can read rather
than take on
trust: `packages/spec/authorable-surface/system.json` gains exactly
`system/DevLogin:email`, `:label`, `:password`, and
`packages/spec/api-surface-declarations/root.txt` gains the two keys on
`ObjectStackDefinitionSchema`.
New exports, from `@objectstack/spec/system`: `DevLoginSchema`,
`DevLogin`,
`DevLoginParsed`.
## Where the declaration belongs, measured
The card said its landing point was measured as `packages/spec` yet
named only two
`packages/cli` files. Both readings are right and they are two different
halves:
- **Declaration: `packages/spec`.** The banner prints once per boot, for
the whole booted
stack, so the key is stack-level — not per-`App` (N apps, one banner)
and not per-package
(the reader looks at the top level). `devPlugins` is the standing
precedent for a
development-only, CLI-consumed, stack-level key.
- **Consumption: `packages/cli`.** `serve.ts` already holds the parsed
definition in the
same scope as the banner call (it reads `config.flows` two lines away,
and
`config.devPlugins` in the same function), so the values reach
`printServerReady` with
no new plumbing.
`DevLoginSchema` itself lives in
`packages/spec/src/system/dev-login.zod.ts`, mounted into
`stack.zod.ts` — the same shape `StackServerConfigSchema` already has.
Two reasons, one
architectural and one mechanical: element schemas live in their domain
file rather than in
the 4000-line envelope, and declaring it inside `stack.zod.ts` displaced
`ObjectStackDefinitionSchema` off the five-export line of
`skills/objectstack-platform/references/_index.md`, dragging a PD-14
governed surface into
this diff for a generated one-line churn. Measured both ways: with the
schema in
`stack.zod.ts`, `check:skill-refs` went red and regenerating produced
that churn; with it
in `system/`, `check:skill-refs` is green and the file is untouched.
**This PR touches no
governed surface** (`docs/adr`, `.claude`, `skills`, `AGENTS.md`,
`CLAUDE.md` — the
`docs/` path here is `docs/audits/`, a regenerated ratchet count).
## Suggestion 1 was not partly landed by #17555 — read first-hand
PR #17555's file list is three files: its changeset,
`packages/cli/src/utils/format.ts`, and one new test. No `packages/spec`
file. Its body
says so in its own words: the card's suggestion 1 "is the right
long-term shape and is
**not** taken here: it needs a new key on a published schema in
`packages/spec`, another
seat's lane". Independently re-derived on `origin/main` at `b7eaf6a61`:
`git grep -nI` for
`devLogins|devHint` printed **no lines at all**; the lit control
(`seededAdmin`, the same
subject) printed 3 files; the dark control (an impossible identifier)
printed none. A second
probe over `packages/spec/src` for any pre-existing dev-credential
declaration
(`devAdmin|devLogin|devCredential|firstRun|devPersona|seedAdmin`)
printed exactly one line,
an unrelated `"u_devadmin"` group key inside an approval-service
docblock; lit control
(`dev`-prefixed keys) printed 4 lines, dark control none.
## Three decisions the card left to the taker
The card listed three, marked "⛔ not prescribed here". Taken, with
reasons:
1. **Where the key lives, and one hint or a list — both, and
stack-level.** They answer
different needs and each has its own composition rule: `devLogins` is
`'concat'`, so
composing two applications keeps both publishers' personas (dropping one
would hide an
audience the composed artifact still serves — the `packages` argument,
one layer over);
`devHint` is `'single'`, so two stacks with contradicting first-run
instructions is a
composition error naming the key, never a silent last-wins. Both are
artifact ENVELOPE
keys: refused inside `packages[].manifest`, exactly as `devPlugins` is,
because the
banner's only reader looks at the top level and a folded copy would be
parsed, stored
and never printed.
2. **Is printing application-supplied credentials safe — yes, with two
constructions.**
The *content* is not an escalation: anyone who can write `devLogins` can
already write
`onEnable`, a function the dev process executes, so these values carry
the trust level
of the code being booted. Two things genuinely are new, and both are
closed by
construction rather than by assurance:
- **Author-controlled text now reaches a TTY.** A terminal obeys control
bytes, so an
escape sequence in a hint could erase the rows above it or repaint a
forged
`🔑 Dev admin` row — the platform's own banner lying on the application's
behalf.
Every C0/C1 byte is replaced with U+FFFD before printing, so an entry
occupies
exactly the rows it was given. Visible characters pass through
unchanged: this is
neutralisation, not redaction, so an author who wrote one can see that
they did.
- **A production boot must not print them.** The block is gated on
`isDev`
(`--dev` or `NODE_ENV=development` — the same condition family that lets
`maybeSeedDevAdmin` fire at all). Any other boot renders
byte-identically to one
declaring nothing, so ADR-0115's attention-budget paragraph is untouched
on the path
it was written about.
The residual, stated rather than hidden: these values are committed to
the
application's repository already. The schema says so where an author
reads it —
development fixture, never a real secret, never a value that also opens
a deployed
environment.
3. **What the banner does when an application declares nothing — exactly
what it does
today.** Byte-identical, asserted for absent, empty-array and
blank-string
declarations, with a positive control proving the same comparison does
move when
something is declared.
One decision the card did not list, taken the same way: the block is
**additive**, beneath
the seeded-admin lines and never instead of them. An
application-controlled key able to
suppress a platform disclosure would let an app hide a live credential
the operator was
just handed. And **declaring is not seeding**: an entry creates no
account, which is why
the banner says the application declared it — an entry naming an
unseeded account then
reads as the app's claim rather than as a broken platform.
## Red / green / dark
New pins: `packages/spec/src/stack-dev-logins.test.ts` (13) and
`packages/cli/src/utils/format.server-ready-app-logins.test.ts` (13).
Rejections assert the
Zod issue `code` and `path`, never a bare throw.
- **Green** — both keys parse, carry their values through `safeParse`
*and* through
`defineStack` (the #8687 defect was a key that parsed green and was then
stripped);
`devLogins` concatenates across a compose; an identical `devHint` passes
through.
- **Red** — an undeclared key inside an entry raises `unrecognized_keys`
at path
`['devLogins', '0']`; `username` and `note` are routed to `email` and
`label` by the
curated aliases; a missing address raises `invalid_type` at
`devLogins.0.email`; a
non-address is refused at the same path; a non-string `devHint` raises
`invalid_type`;
two differing hints throw naming `top-level key 'devHint'`.
- **Dark** — `devLoginz`, `devHints` and `devCredentials` still raise
`unrecognized_keys`
at path `[]`, so the green above reads as "two keys were declared" and
not as "the strict
close regressed". On the banner side, control-byte-free text passes
through with no
U+FFFD anywhere, so "the bytes were replaced" is a measurement.
**Ablations** (`scripts/ablation-replace.mjs`, on-disk proof and restore
verified by blob
hash plus an empty `git diff HEAD` each time):
| mutation | anchor | result |
|:--|:--|:--|
| drop `.email()` from `DevLogin.email` | 1 to 0, blob `d6b5d27f8e38` to
`b0c0abd3c80c` | exactly **1 of 13** red — the address pin. The other 12
stay green, so the ablation is attributable rather than blunt |
| delete the `devLogins` key from the stack shape | 1 to 0, blob
`11f3c284217a` to `b2611a37d33f` | **8 of 13** red — every accept,
refuse and compose pin. The accept leg is not vacuous |
**Cross-package reverse verification** — `packages/cli` types against a
schema built in
`packages/spec`, so the green had to be shown to be a reading of the
rebuilt `.d.ts` and
not a cache. Replacing `entry.email` with `entry.nope` in `format.ts`
(anchor 1 to 0, blob
`8143c65513c9` to `1375d1655de6`) turned `pnpm --filter @objectstack/cli
typecheck` red
with `TS2339: Property 'nope' does not exist on type '{ email: string;
password?: string |
undefined; label?: string | undefined; }'` — the message quotes the
exact shape declared in
`system/dev-login.zod.ts`. Restored, blob back to `8143c65513c9`, `git
diff HEAD` empty,
typecheck green again.
## Verification
Measured at `53f78a5796`, this branch.
- `pnpm --filter @objectstack/spec test` — **495 files / 14534 tests
pass**; `typecheck` green.
- `pnpm --filter @objectstack/cli exec vitest run --project unit` —
**218 files / 3077 tests pass**; `typecheck` green. The `integration`
tier is declared to CI (`pnpm test` runs both); the diff adds no spawn
entry point and changes no boot behaviour beyond two option values.
- `pnpm --filter @objectstack/spec check:generated` — **16 of 16
artifacts up to date**.
- `pnpm lint` (`eslint . --no-inline-config`, the whole repository, not
a narrowed run) — **green in 78s**.
- `node scripts/pm/dispatch-gates.mjs --ran` — **131 derived families
accounted for: 131 run, 0 NOT MEASURED.** ⭐ Re-derived and re-run at
head `9b0cba8b19` in the patch round, every family exit 0; the
reconciliation reads it back as a DERIVED zero (all 131 recorded an exit
code and none of them is 3). devHint / devLogins[]) (#19139)1 parent 7e40d1d commit 24d622b
28 files changed
Lines changed: 912 additions & 30 deletions
File tree
- .changeset
- content/docs
- getting-started
- references
- system
- docs/audits
- packages
- cli
- src
- commands
- utils
- test
- fixtures
- spec
- api-surface-declarations
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- src
- system
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
88 | | - | |
| 88 | + | |
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | | - | |
| 36 | + | |
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| |||
316 | 316 | | |
317 | 317 | | |
318 | 318 | | |
319 | | - | |
| 319 | + | |
320 | 320 | | |
321 | 321 | | |
322 | 322 | | |
| |||
329 | 329 | | |
330 | 330 | | |
331 | 331 | | |
| 332 | + | |
332 | 333 | | |
333 | 334 | | |
334 | 335 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
264 | 264 | | |
265 | 265 | | |
266 | 266 | | |
267 | | - | |
| 267 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5222 | 5222 | | |
5223 | 5223 | | |
5224 | 5224 | | |
| 5225 | + | |
| 5226 | + | |
| 5227 | + | |
| 5228 | + | |
| 5229 | + | |
| 5230 | + | |
| 5231 | + | |
| 5232 | + | |
5225 | 5233 | | |
5226 | 5234 | | |
5227 | 5235 | | |
| |||
0 commit comments