Uh oh!
There was an error while loading. Please reload this page.
docs(spec): correct AutomationContext.flowName's attribution prose — elevation decides authorization, not attribution - #14035
Conversation
…14011) The published contract said a `runAs:'system'` run "resolves no user", so the `svc:flow:<flowName>` actor label stands in for the audit row's attribution. That reads as "system elevation costs you the operator in the audit trail" and has not been true since #5494: `resolveRunDataContext` carries the triggering user through unchanged, `isSystem` alone decides authorization, and the audit writers record `session.userId ?? session.actor` with no `isSystem` gate. The `svc:flow:` label is the FALLBACK for a genuinely user-less run (a schedule). Runtime behaviour is untouched. `runas-attribution-contract.test.ts` pins both limbs against the real ObjectQL stack so the next drift goes red on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c6078e63e62fafba017369a374d2836498d0139f && git checkout c6078e63e62fafba017369a374d2836498d0139f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0d80862366f5181b5db75c5ba103c69db98df181 aca37be257b3e88742e7b16ef84b61e779219f7c && git checkout -B drift-repro 0d80862366f5181b5db75c5ba103c69db98df181 && git merge --no-ff aca37be257b3e88742e7b16ef84b61e779219f7c
node scripts/docs-audit/affected-docs.mjs --json 0d80862366f5181b5db75c5ba103c69db98df181
|
hotlong
commented
Sep 1, 2026
Docs Drift Check 的两行都查过了,结论分开:一行是误报,一行是真的,已按守则立卡而非在此编辑。 本 PR 未触碰
那条真的
无限定,读起来就是所有 system 提权运行的通则 —— 也就是本 PR 正在契约散文里消灭的那个错概念。 而 同一个发布页 的
两条各自都不假(一条讲 actor 标签、一条讲落库的三列),但并排读,靠前那条在靠后那条之前约 800 行,中间没有任何东西把它们连起来。front-to-back 读 v17 的人会先遇到无限定版本。 ⛔ 不建议直接改写 #4366 那条 —— 那会篡改它当时确实交付的东西。#14039 里给了两个保持记录诚实的选项(加限定+指引 / 依赖本 PR 修好的契约散文),并说明发布说明政策是发布流程的事,不是下游报告人的,所以我没有提 PR。 关于 dev 的扫描范围派发词明令禁止编辑 Generated by Claude Code |
hotlong
commented
Sep 1, 2026
CI 全绿,交接给 spec 席位。 head 保持 draft,我不转 ready 也不合 —— 这是本仓契约面的裁量权,不属于一个下游 PM 席位。我这边对本 PR 的驱动到此为止。 要落它需要知道的三件事,集中在这里免得翻楼:
与 #14044(#14010 半 B)不重叠:那张改 CLI/lint/runtime,本张只改 spec 契约散文与一个新测试。 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14011
A prose-only correction. The shipped runtime behaviour is right and is untouched — the sentence describing it was not.
The stale sentence
packages/spec/src/contracts/automation-service.ts, documentingAutomationContext.flowName(published asdist/contracts/index.d.ts, line 3033 on 17.1.0 / line 3000 on this branch's 17.2.0 build):That reads as "system elevation costs you the operator in the audit trail."
What actually ships — re-verified against
origin/main, not taken from the cardThe card's reading was measured on the 17.1.0 artifact; every claim below was re-derived from the source in this checkout at
62a137b.packages/services/service-automation/src/runtime-identity.ts→resolveRunDataContext,runAs:'system'branch:{ isSystem: true, actor: 'svc:flow:' + flowName, ...(context.userId ? { userId: context.userId } : {}), ...(context.tenantId ? { tenantId } : {}), positions: [], permissions: [] }. The operator is carried through unchanged (Automationcreate_recordunderrunAs:'system'inserts rows withowner_id/organization_id/created_byall NULL — records born untouchable even by admin #5494) — the comment on that line says so in as many words: "elevation is not anonymity …isSystemalone decides authorization, while the user drives the platform's attribution stamps."packages/objectql/src/plugin.ts→sys_stamp_audit_insert/sys_stamp_audit_updatestampcreated_by/updated_byunderif (session?.userId). There is noisSystemtest anywhere on that path.packages/plugins/plugin-audit/src/audit-writers.ts→writeAuditcomputesuserId = sess.userId ?? provenance.attributedUserIdandactorLabel = userId ?? sess.actor. Thesvc:flow:label is the fallback, in that order, with noisSystemgate.So the
svc:flow:labelling the sentence described is real — but it is the user-less case (a schedule, or arunAs:'system'flow fired by a write that itself carried no user). The sentence generalised it to everyrunAs:'system'run.What changed
1. The prose (
packages/spec/src/contracts/automation-service.ts). TheflowNameJSDoc now states what is true of both cases: elevation decides AUTHORIZATION and leaves ATTRIBUTION alone; thesvc:flow:actor label is the fallback a genuinely user-less run lands on, where ADR-0118 D1 keeps the user column null rather than inventing a sentinel.2. A sweep for the same wrong concept. Searched
packages/spec/src, every package's JSDoc, andcontent/docs/for the family —resolves no user,unattributed,user-less,Unknown user,elevation/anonymity, and every prose mention ofrunAs: 'system'andsvc:flow:. Exactly one site carried the wrong generalisation, the one fixed here. Everything else that mentions a user-less run is already scoped to the genuinely user-less case and was deliberately left alone (for instancecontent/docs/permissions/system-context.mdxline 200, "a user-less system write stamps nothing", andservice-automation/src/engine.tsline 3436, "for runs that resolve no user" — both correct as written).3. A line between the prose and the code that rings.
packages/services/service-automation/src/runas-attribution-contract.test.tsasserts the invariant the prose now states, at the end of the chain — the session envelope the audit writers actually read — rather than atresolveRunDataContext's return shape, whichbuiltin/crud-runas.test.tsalready pins:updated_bymoves to the triggering operator — the same value the identical write produces on the plain user path, withcreated_byleft on the creator;session.actorissvc:flow:night_sweep.If the code ever becomes what the old prose described, this file goes red instead of the next reader having to re-measure.
Reverse verification — and what it caught
Direction predicted before running: the operator limb goes RED, the user-less limb stays GREEN (it has no user to strip).
Ablation A — deleted the
userIdcarry-through fromresolveRunDataContext's system branch (i.e. made the code match the old prose). The first run reddened only the hook-session assertion; theupdated_bycolumn assertion passed under mutation, because the row had been seeded by the operator and already carried that value from its own insert. That assertion was measuring the insert, not the elevated update. Fixed by seeding the rows as a different user (usr_creator), so the column has to move for the assertion to pass. Re-ablated:Ablation B — replaced the
svc:flow:label with a constant, to check the second limb is live too:Both ablations mutated a committed tree, proved the mutation on disk by grep counts plus a
git hash-objectcomparison against the HEAD blob, carried an absolute-pathtrap … EXIT INT TERM, and restored to the byte-identical HEAD blob (git diff HEADempty) before the next measurement. No rebuild step was needed for either:runtime-identity.tsis reached frombuiltin/crud-nodes.tsby a relative source import inside its own package, and this package's vitest config aliases only@objectstack/platform-objects— nothing routes its own modules throughexports/dist.Verification
All readings below are from
aca37be, the branch head; the gate union was run after the final commit.@objectstack/service-automationfull suite@objectstack/specfull suite@objectstack/spectsc --noEmit@objectstack/service-automationtsc --noEmitTS2341innested-region-parity.test.ts; 0 from the new file, and--listFilesconfirms the new test file is in that programpnpm lint(repo-wideeslint . --no-inline-config)scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands)The three non-green results are all exit 3 / PREREQUISITE NOT MET — nothing measured, not findings:
check:type-check-debtandcheck:dual-build-cjs-loadsandscripts/check-dev-prereqs.mjsall refuse without a fully built workspace closure. CI builds it before those steps, exactly aslint.ymldoes. The substance the ratchet guards was measured directly instead, in thetsc --noEmitrow above: the new test file is in the program and contributes zero errors, so the@objectstack/service-automationentry (errors: 3) does not move.scripts/check-test-completeness.mjs(exit 3) needs a savedturbo run testlog it cannot produce itself, andscripts/pm/check-half-states.mjs(exit 3) needs repo-scoped egress this container does not have. Both declare that exit as NOT MEASURED.packages/specwas rebuilt after the edit, and the corrected sentence is present indist/contracts/index.d.ts(1 occurrence) with the stale one gone (0 occurrences).Notes for the reviewer
domain:*label set: this was dispatched by the downstreamsteedos-labs/hotcrm-heimaoPM seat under a direct maintainer instruction, not by this repo's spec lane. The spec seat or the maintainer lands it..changeset/automation-context-flowname-attribution-prose.mdbumps@objectstack/specpatch — the correction ships in the published.d.ts.@objectstack/service-automationgets no bump: its only change is a new test file.editable: false— the guard and the writer are the same door #14010, which addsrunAstoHookSchemaand changes runtime.HookSchemais untouched here, and nothing in this prose forward-announces a capability that has not landed.Generated by Claude Code