Skip to content

fix(cli,plugin-auth): one boot line per warning class, each printed once, and the loopback OAuth notice at info - #22097

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-22073-boot-warning-one-line-per-class
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-22073-boot-warning-one-line-per-class

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 Fix fumadocs-mdx validation: flatten nested pages in concepts meta.json #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:

  ⚠ 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.

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

claude added 5 commits October 7, 2026 13:55
…withholds what the banner restated

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
…ack issuer, warn on private and link-local

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
…st the real automation plugin

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
…e with the banner

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
claude added 2 commits October 7, 2026 16:14
…ssets branding warning's shape

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 36 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bafb58bb08aa3989b1d72a633496b361fcf0842e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1d155ac9d999bb9a3af743f4820e839a20183d7 — the merge of head d722b4746e2a7f3bf55b53efb7f9d51a276e0c9c into base bafb58bb08aa3989b1d72a633496b361fcf0842e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a1d155ac9d999bb9a3af743f4820e839a20183d7 && git checkout a1d155ac9d999bb9a3af743f4820e839a20183d7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bafb58bb08aa3989b1d72a633496b361fcf0842e d722b4746e2a7f3bf55b53efb7f9d51a276e0c9c && git checkout -B drift-repro bafb58bb08aa3989b1d72a633496b361fcf0842e && git merge --no-ff d722b4746e2a7f3bf55b53efb7f9d51a276e0c9c

node scripts/docs-audit/affected-docs.mjs --json bafb58bb08aa3989b1d72a633496b361fcf0842e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 37655130539 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • (没拿到 job 级信息,点上面的 run 链接看)

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 2 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants