Skip to content

fix(service-print): a server print works under the console's default CSP (the ready wait passes a function, not a string) - #26783

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-jkp4vn
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-jkp4vn

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #26664

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:服务端打印的 Chromium 驱动等待打印页就绪时,改为传一个函数谓词,不再传字符串。
  • 为什么改:默认部署下控制台 CSP 不含 'unsafe-eval',字符串谓词每次轮询都要在页面里 eval,第二次轮询即抛 EvalError,所有服务端打印都失败。
  • 风险与回滚:只动 chromium-driver.ts 一处等待;就绪含义、超时、轮询间隔不变,控制台 CSP 不变。回滚即还原该文件。
  • 先看:packages/services/service-print/src/chromium-driver.ts(printReady 与 waitForFunction 那一行);chromium-driver.unit.test.ts 末尾新增的两条。
  • 两分钟验证:OS_TEST_PRINT_CHROMIUM_PATH=/opt/pw-browsers/chromium pnpm --filter @objectstack/service-print exec vitest run src/chromium-driver.test.ts,应见 CSP 那条在两种放置下都通过(main 上是 EvalError)。
  • 需要你做的:无,席位落地。

What changes

packages/services/service-print/src/chromium-driver.ts waited for the print page with page.waitForFunction(PRINT_READY_PREDICATE, …), where the predicate was a string. It is now a function, printReady(stablePolls), passed as page.waitForFunction(printReady, STABLE_POLLS, { timeout: remaining(), polling: POLL_INTERVAL_MS }).

Which evaluation path, and why script-src does not block it (read in playwright-core 1.63.0, lib/coreBundle.js waitForFunctionExpression, and measured below). The wait installs a poller through the injected script's evaluateHandle, a DevTools Runtime.callFunctionOn. The poller's predicate() runs globalThis.eval(expression):

  • with a string (isFunction: false), on every poll. The first poll runs synchronously inside the DevTools call, where the inspector permits code generation; every later poll runs from a page setTimeout, where the page's CSP governs, and without 'unsafe-eval' it throws EvalError. The predicate needs three stable polls, so the second poll always fails.
  • with a function (isFunction: true), once: the first, synchronous poll caches the evaluated function (evaledExpression), and every later poll calls that cached function. No string is evaluated from a page timer, so the CSP has nothing to refuse.

The readiness meaning is unchanged: the same three conditions (a [data-print-page] root, window.__objectui present, idle === true), the same in-page run counter (__objectstackPrintStable), the same STABLE_POLLS (3), the same 100 ms polling and the same share of the one render budget. The page receives the function's text only, so the poll count it used to read from a template literal now arrives as its argument. The built output was read: tsup emits printReady as plain ES2020 in both dist/index.js and dist/index.cjs, with no injected helper names.

The console CSP (packages/cli/src/utils/console.ts) is not touched. page.evaluate(FONTS_READY) stays a string: it is evaluated synchronously inside its DevTools call, and the CSP leg below shows it passing.

A patch changeset on @objectstack/service-print.

Tests

  • chromium-driver.unit.test.ts (runs in CI, no browser): the staged page's waitForFunction must receive a function, STABLE_POLLS as its argument, polling: 100 and a timeout inside the budget. A second case runs the received function against a staged document / __objectui and pins the meaning: false with no console, false when idle with no root, false with the root while busy, then exactly STABLE_POLLS idle polls to true, and a busy poll resets the run.
  • chromium-driver.test.ts (real Chromium, both placements): the fixture now also serves the print page under the console's default policy (script-src 'self' 'nonce-…' 'wasm-unsafe-eval', the script carrying the nonce), and a new case per placement renders it.
  • The render dogfood (packages/qa/dogfood/test/print-service-render.dogfood.test.ts, which carries the named-page leg from feat(service-print): print a record on any print page of its object, not only its default record page #26662) is unchanged; it is the end-to-end reading below.

Acceptance notes

  • CI skips both real-Chromium files (describe.skipIf(!CHROMIUM) / !CHROMIUM || !CONSOLE_BUILT): Test Core has no Chromium and no console bundle. The unit test is the guard CI runs. Turning the render tests on in CI is a CI-provisioning question and is not done here.
  • page.evaluate(FONTS_READY) and the predicate's first poll rely on DevTools-initiated evaluation being exempt from the page's 'unsafe-eval' check, which is Chromium's behaviour today and is what playwright-core's own function-predicate path relies on.

Verification

All readings at head dfd3fc087, with OS_PAGE_REACT_ENABLED (and legacy OS_PAGE_REACT) unset, Chromium /opt/pw-browsers/chromium, console bundle from pnpm console:build.

Red on main's driver (base 1685a06): EvalError

chromium-driver.ts byte-equal to origin/main, the new tests committed:

  • vitest run src/chromium-driver.unit.test.ts src/chromium-driver.test.ts: Tests 4 failed, 16 passed (20). Both unit cases fail; the CSP case fails on both placements with The print engine failed at the local executable /opt/pw-browsers/chromium: page.waitForFunction: EvalError: Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script: script-src 'self' 'nonce-print-fixture-nonce' 'wasm-unsafe-eval' (and the same at the remote endpoint).
  • Dogfood print-service-render.dogfood.test.ts + print-service-named-page.dogfood.test.ts: Tests 6 failed, 6 passed (12). All 6 render legs (invoice, named print page, revoke-after-failure; local and remote) fail with the EvalError under the console's real nonce policy. The named-page file uses a stand-in driver and passes either way.
Green with the fix
  • Same two driver files: Test Files 2 passed, Tests 20 passed (20).
  • Same two dogfood files: Test Files 2 passed, Tests 12 passed (12).
  • pnpm --filter @objectstack/service-print build, typecheck: exit 0. test (with Chromium): Test Files 8 passed, Tests 118 passed (118).
  • pnpm exec eslint --no-inline-config packages/services/service-print: exit 0.
Gates

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at dfd3fc087 (4 paths, +129 / -13): 59 commands, plus the 4 roster gates it marks as keeping a roster under one of these paths (check:authz-resolver, check:changeset-fixed, check:error-code-casing, check:filter-alias-parity). Each run alone, exit code captured before any pipe:

  • 61 of 63 exit 0 on the first run.
  • node scripts/check-plugin-teardown-shape.mjs --self-test: exit 3 on the shallow clone (its pinned fixture commit 621a4876 was not in the object store). After git fetch origin 621a487607881c66b2899b7e3477115229a156b4: exit 0, 48 cases pass.
  • pnpm check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET (7 untouched packages had no dist/). After building those 7: exit 0, 112 require entry points across 69 packages load.

dispatch-gates --ran with every exit code recorded: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN (exit 0).

origin/main moved 11 commits since the base, none touching packages/services/service-print, the print dogfood or packages/cli/src/utils/console.ts; git merge-tree of head with it is clean. Every family CI owns (Lint & Repo Gates, Test Core, the type-check lanes) is read from this PR's checks.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code


Generated by Claude Code

…t CSP on both placements

Claude-Session: https://claude.ai/code/session_01FEkQ5kpFFUuQY5FA7FCdwY
Co-authored-by: Claude <noreply@anthropic.com>
…n with the stable-poll count

Claude-Session: https://claude.ai/code/session_01FEkQ5kpFFUuQY5FA7FCdwY
Co-authored-by: Claude <noreply@anthropic.com>
…, so a print runs under the console's default CSP

playwright-core evaluates a string predicate with eval on every poll, from a page
timer the page's CSP governs; without 'unsafe-eval' the second poll threw EvalError
and every server print failed. A function predicate is evaluated once, inside the
DevTools call that starts the wait, and each poll calls it. Same predicate, same
timeout and polling; the stable-poll count is now its argument.

Claude-Session: https://claude.ai/code/session_01FEkQ5kpFFUuQY5FA7FCdwY
Co-authored-by: Claude <noreply@anthropic.com>
… CSP; keep the unit test inside the package's lib

Claude-Session: https://claude.ai/code/session_01FEkQ5kpFFUuQY5FA7FCdwY
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 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 55 of 207 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 — 4 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 785a7c3b3e31a669ef52e0d83bc18916d170ae9c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f23682269958467fb4e615289bbf80f46b26b81f — the merge of head dfd3fc087575ed9e16be6e1678acca0ab21e561d into base 785a7c3b3e31a669ef52e0d83bc18916d170ae9c, 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 f23682269958467fb4e615289bbf80f46b26b81f && git checkout f23682269958467fb4e615289bbf80f46b26b81f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 785a7c3b3e31a669ef52e0d83bc18916d170ae9c dfd3fc087575ed9e16be6e1678acca0ab21e561d && git checkout -B drift-repro 785a7c3b3e31a669ef52e0d83bc18916d170ae9c && git merge --no-ff dfd3fc087575ed9e16be6e1678acca0ab21e561d

node scripts/docs-audit/affected-docs.mjs --json 785a7c3b3e31a669ef52e0d83bc18916d170ae9c

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-27T15:44Z

Head dfd3fc0875, report 6446107368 (on #26664), claim 6444477647. No contract review is owed: there is no packages/spec/** diff, no governed path, no narrowing and no Clause-②: yes.

item reading
Shape Draft, base main, Fixes #26664 under triage 6442489247 (p1). 4 files in packages/services/service-print/src/: chromium-driver.ts, its two tests, and a patch changeset. Clause-②: no and Metadata-protocol: none appear in both the body and the changeset.
Fix The ready wait passes a function (printReady, with STABLE_POLLS as its argument) instead of a string. Playwright evaluates a string predicate again on every poll from a page timer, where the page's CSP applies, so under the console's default script-src (no 'unsafe-eval') every print failed with EvalError. A function's text is evaluated once inside the DevTools call that starts the wait, which the page's CSP does not govern. The predicate's meaning, timeout and polling are unchanged; the console CSP is untouched, as the triage requires. The FONTS_READY string stays: evaluate runs it once through DevTools, not from a page timer.
Evidence CI green on head dfd3fc0875 (33 success, 11 skipped, 0 failed). Governed check: NOT governed, 142 changed lines. Red on main under a default-CSP fixture: the driver test 4 failed (EvalError, both placements) and the render dogfood 6 of 12; green at head: driver 20/20, dogfood 12/12, service-print 118/118, with OS_PAGE_REACT_ENABLED unset. 59 of 59 derived gates ran, 0 NOT-MEASURED. The unit test pins that the driver hands waitForFunction a function, so CI guards the regression although it skips the real render.
Downstream With #26583 (PR #26662), this unblocks hotcrm #2312 once both ship in a release.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, once every check on this head is green or an expected skip.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 15:46
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 15:46
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 665059b Oct 27, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-jkp4vn branch October 27, 2026 16:10
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/m tests tooling

Projects

None yet

2 participants