Repository navigation
Commit 4935c66
fix(cli,plugin-auth): one boot line per warning class, each printed once, and the loopback OAuth notice at info (#22097)
Fixes #22073
Clause-②: no
## What changes
Three boot-output changes, as the triage ruling on the card (comment
`6038562452`) directs. Log text and log levels only: no accept set,
answer or public signature moves.
1. **One line per warning class.** `printAutomationSummary`
(`packages/cli/src/utils/format.ts`) groups the engine's binding audit
by (trigger type, reason) and prints one line per class with its flows
listed. The class's short text is the reason's **first sentence**,
derived by `leadSentence()`. It is never a second, hand-written wording:
the engine owns every reason sentence (`describeUnboundReason`, and
`scheduledWorkDisabledReason` for the deployment policy). A reason that
is already one sentence (a missing trigger, a binding failure, a
declined subflow) prints whole, remedy included. Distinct reasons stay
distinct lines.
2. **Each warning prints once.** `printAutomationSummary` now returns
the identifying text of every logger record it restated, at the moment
it prints its own line. `printBootDiagnostics` withholds exactly those
and counts them in its header: `⚠ Boot diagnostics — 3 warnings logged
during startup (2 more already listed above):`. The shadowed-flow
bootstrap restatement is covered by the same rule. The pull-time `Flow
name collision` record still replays, because it says which rule armed
the body.
3. **Loopback OAuth notice at `info`.** In
`packages/plugins/plugin-auth/src/auth-plugin.ts`, `OAuth is served
UNENCRYPTED: …` logs at `info` when the issuer's host is loopback and
stays `warn` for private and link-local hosts. The sentence is the same
and is still emitted on every accepted plain-HTTP boot, so D1 of ruling
batch #210 item 5 ("eligibility decides WHICH sentence, never WHETHER")
holds across both levels.
Where the long text now prints: at `--log-level debug` (or `info`) the
boot-quiet window never opens. The live stream then carries
`@objectstack/service-automation`'s per-flow `[Automation] flow 'NAME'
declares a 'TYPE' trigger but is NOT bound — it will never auto-launch.
REASON` warning, with the whole reason. When any class line was
shortened, one dim line under the classes says so.
### Measured, before → after (H1)
`examples/app-todo` has 2 package-authored `schedule` flows. Booted with
`os dev --fresh --no-watch`, scheduled work off (the default), default
log level:
| | before (`3d918850`) | after (this branch) |
| --- | --- | --- |
| lines carrying the schedule `NOT bound` sentence | **4** (2 banner + 2
*Boot diagnostics*) | **1** |
| `OAuth is served UNENCRYPTED` lines (localhost) | 1 (`WARN`, in *Boot
diagnostics*) | 0 |
| *Boot diagnostics* records shown | 6 | 3 (+2 counted as listed above)
|
The after banner line:
```text
⚠ 2 flows declare a 'schedule' trigger but are NOT bound — disabled by deployment policy — package-authored scheduled work is off on this deployment (OS_AUTOMATION_SCHEDULED_WORK_ENABLED is unset or not truthy), so no time trigger arms and no packaged `defineJob` is scheduled: task_reminder, overdue_escalation
reasons cut to their first sentence — --log-level debug prints each flow's full reason
```
The second producer was the one the PM named: the banner's `a.unbound`
loop, plus *Boot diagnostics* replaying `service-automation`'s
`kernel:bootstrapped` audit warning (`plugin.ts:1533`). There was no
third copy. By construction, the hotcrm shape (8 flows) goes from 16
lines to 1 class line plus 1 hint line. hotcrm itself was not booted
here.
### How a new boot warning stays single (H7)
*Boot diagnostics* withholds only a record that a banner section
**handed back as restated when it printed its own line**. Today the only
such section is the automation summary, and only for the
service-automation audit records it summarises.
- A new `logger.warn`, such as the branding-asset warning #22071 adds,
is restated by no banner section. It is never matched, so it replays
once.
- A banner line that is not also a logger record has nothing to
withhold, so it prints once.
- The match can fail only toward printing. A reworded producer or an
escaped character in a JSON-format record is printed, never dropped, so
the failure mode is a doubled line, never a lost diagnostic.
- A banner section that starts restating a logger record must hand that
record back, or it doubles. That contract is documented on
`BootDiagnosticsReplayOptions.restatedAbove`.
- The failed-boot and migrate-and-exit paths print no banner and pass
nothing, so they replay every record.
## PM readings, measured
- **H1**: reproduced with the counts above. The two producers are the
ones named.
- **H2**: the CLI does both jobs, and `service-automation` is untouched.
Hosts that boot the kernel without the CLI banner read the service's
warning as their only notice:
`packages/runtime/src/domains/automation.ts` (the runtime composition
every adapter host uses), `packages/verify/src/harness.ts`, and the
plugin's own comment at `plugin.ts:1490`–`:1492` ("The warn matters for
embedded hosts and tests"). Lowering that warning, or grouping it, would
change what those hosts see. No `@objectstack/service-automation`
changeset.
- **H3**: classes are keyed on the whole reason, not its short form. The
deployment-policy reason is one class per trigger type. A real binding
failure, a missing trigger and a declined subflow each keep their own
line. The short-text derivation and the long-text location are described
above.
- **H4**: no other published origin feeds the decision.
`getAuthIssuer()`, `getMcpResourceUrl()` and `isMcpOAuthEnabled()` all
derive from the one `getCanonicalOrigin()`, so "every published origin
is loopback" reads as "the issuer's host is loopback". **Partly
falsified:** the transport rule has no separable loopback predicate to
reuse. `isOAuthEligibleBaseUrl` checks `localhost` / `*.localhost`
inline (`auth-manager.ts:558`), and `isPrivateOrLoopbackHostLiteral`
checks `127.0.0.0/8` and `::1` in the same function as the private
ranges. Extracting one means editing `auth-manager.ts`, which is outside
this card's declared file surface. So the notice's module-local
`isLoopbackIssuer()` composes the rule's own pieces: the same
`localhost` / `*.localhost` names, the `127.0.0.0/8` block judged by the
same ADR-0069 D5 matcher (`ipMatchesRange`, already exported), and
`[::1]`. It adds no new host regex, and it reads the WHATWG-canonical
hostname as the rule does. It is asked only of an issuer the rule
already accepted. Agreement is pinned by the loopback / private table
below. Extracting one shared predicate is left as an open question in
the report.
- **H5**: two assertions in `mcp-oauth-plaintext-notice.test.ts` flip,
both for the ruling's reason. `fires on a loopback deployment too —
plain HTTP is plain HTTP` (localhost: 1 `warn`) is replaced by `logs the
sentence at info, never warn, on a LOOPBACK deployment`, over
`localhost`, `app.localhost`, `127.0.0.1`, `127.0.0.2`, `127.1` and
`[::1]`, each 0 `warn` and 1 `info` naming the issuer. `every plain-HTTP
boot gets exactly one of the two sentences` now counts across both
levels; its localhost row would otherwise read 0. The control stays
`warn`: `is emitted at warn on a private address` (`172.16.0.1`), plus a
new `keeps the warning on a PRIVATE or LINK-LOCAL deployment` over
`10.0.0.5`, `172.16.0.1`, `192.168.1.10`, `169.254.10.20`, `[fd00::1]`
and `[fe80::1]`.
- **H6**: `packages/cli/src/commands/doctor.ts:289` is unchanged. Text
that quoted the old lines moved with them, outside `content/docs/**`:
- `docs/qa/platform-checklist/areas/platform-core.json`: the
`platform-core.boot-health` acceptance `verify` quoted the per-flow
banner line. Revision 6 → 7, with a history entry.
- `docs/qa/platform-checklist/areas/ai.json`:
`ai.mcp-oauth-private-host-transport` expected the accepted-transport
line "exactly 1 on (c)" (loopback) at the default level. It now names
the level per boot, and boot (c) runs at `--log-level info`. Revision 2
→ 3, with a history entry. `pnpm check:platform-checklist` is green.
- No `content/docs/**` page needed an edit. `automation/flows.mdx:2380`
and `deployment/environment-variables.mdx:91` say such flows are "listed
by … the CLI startup summary as disabled by deployment policy", which is
still true. `releases/v17/17-5.mdx` is release-owned and untouched.
`packages/spec/scripts/publish-smoke-boot-failure.test.ts` holds
verbatim historical specimens of the old header with no restated
records, which are still valid.
- **H7**: covered in the section above.
## Tests (at `de6b4a08`)
- New `packages/cli/src/utils/format.boot-warning-classes.test.ts`, unit
tier, 11 tests. The first block boots the **real**
`AutomationServicePlugin` on a `LiteKernel` with 8 `schedule` flows and
scheduled work off. It captures stdout through the same `BootLogCapture`
`serve` uses, collects through `collectAutomationSummary`, and prints
the real banner. Pins:
- exactly **one** line carries `a 'schedule' trigger`, and it names all
8 flows;
- every flow is named on exactly **one** line, so no warning appears
twice;
- the long explanation is absent at the default level.
The premise is asserted first: the producer warned once per flow into
the capture. Formatter legs cover distinct classes, the first-sentence
cut, the hint only when shortened, a JSON-format record, a record the
banner did not restate (never withheld), the shadowed restatement
withheld while the collision record is kept, and the no-banner path
replaying everything.
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: **260
files, 3797 passed**. `pnpm --filter @objectstack/cli typecheck`: exit
0. The new file is in `tsconfig.json`'s program (`--listFilesOnly`), and
`check:test-typecheck` is OK. The `integration` tier is declared to CI:
the diff touches no spawn entry, no integration-tier file and no driver.
- `pnpm --filter @objectstack/plugin-auth test`: **126 files, 2623
passed, 10 skipped**. `pnpm --filter @objectstack/plugin-auth
typecheck`: exit 0.
- **Ablations.** Each mutation went through
`scripts/ablation-replace.mjs` against the committed fix, with the
anchor hit verified on disk and the restore proven as blob == HEAD with
an empty `git diff HEAD`. The subjects resolve from source, so no
rebuild was needed.
- **A**: drop the unbound claim. 5 red: both real-boot pins, plus the
three formatter print-once legs that withhold unbound records.
- **B**: key classes by flow. 2 red: the one-schedule-line pin and the
classes leg.
- **C**: never log at `info`. 6 red: every loopback spelling.
- **D**: treat every host as loopback. 10 red: every private and
link-local control.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --ran` reports **72
derived, 72 run, 0 NOT-MEASURED, 0 UNRUN** at `de6b4a08`. The derivation
added `pnpm --filter @objectstack/lint run
check:doc-formula-expressions` and `pnpm check:platform-checklist` to
the PM's list. `check:dual-build-cjs-loads` and `check:i18n-coverage`
first exited 3 (PREREQUISITE NOT MET, unbuilt workspace packages). After
`pnpm turbo run build`, they and the other dist-reading gates were
re-run, all exit 0. Full `pnpm lint` exits 0 at `de6b4a08`. Narrowed
eslint (`--no-inline-config --format json`) over the 4 changed TS files:
0 errors, 0 warnings. The other 3 changed files are outside eslint's
population ("no matching configuration"), and the config enables no
type-aware linting (`eslint.config.mjs:327`).
## Acceptance notes (not filed)
- *Boot diagnostics* still pairs the ready line `⚠ Server is ready —
DEGRADED: missing core services: …` with the kernel's `System started
with degraded capabilities …` record. This is deliberately left: it is
the ready-line status, not a banner-list entry, and
`serve-ready-degraded-boot.e2e.test.ts` asserts the kernel sentence in
the same output as its premise.
- A flow declined onto a disabled packaged subflow also has a bind-time
engine `warn` (`Flow 'NAME' is registered but NOT armed on trigger
'TYPE' — …`). That is a different record from the bootstrap audit, so it
still replays beside the banner's class line. The case is rare, and the
record carries the subflow detail.
- The service's own bootstrap sentence reads "is NOT bound — it will
never auto-launch. disabled by deployment policy — … This is not a
binding failure" for an embedded host. That is a wording observation,
unchanged here (H2).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c28f317 commit 4935c66
7 files changed
Lines changed: 641 additions & 30 deletions
File tree
- .changeset
- docs/qa/platform-checklist/areas
- packages
- cli/src/utils
- plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
690 | 690 | | |
691 | 691 | | |
692 | 692 | | |
693 | | - | |
| 693 | + | |
694 | 694 | | |
695 | 695 | | |
696 | 696 | | |
| |||
714 | 714 | | |
715 | 715 | | |
716 | 716 | | |
717 | | - | |
| 717 | + | |
718 | 718 | | |
719 | 719 | | |
720 | 720 | | |
| |||
731 | 731 | | |
732 | 732 | | |
733 | 733 | | |
734 | | - | |
| 734 | + | |
735 | 735 | | |
736 | | - | |
| 736 | + | |
737 | 737 | | |
738 | 738 | | |
739 | 739 | | |
| |||
783 | 783 | | |
784 | 784 | | |
785 | 785 | | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
786 | 792 | | |
787 | 793 | | |
788 | 794 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
93 | 99 | | |
94 | 100 | | |
95 | 101 | | |
| |||
0 commit comments