You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Stale contract prose: AutomationContext.flowName says a runAs:'system' run "resolves no user", but #5494 made elevation carry the operator through #14011
Observation, filed from a downstream app (steedos-labs/hotcrm-heimao). Not a runtime defect — the shipped behaviour is right and the prose describing it is not.
a runAs:'system' run resolves no user, so resolveRunDataContext labels its data operations svc:flow:FLOWNAME on ExecutionContext.actor … instead of leaving the audit row unattributed
What ships
@objectstack/service-automation's resolveRunDataContext does the opposite, and says so on purpose (#5494):
elevation is not anonymity … carry them through: isSystem alone decides authorization, while the user drives the platform's attribution stamps
Measured on 17.1.0: a write with { ...callerCtx, isSystem: true } leaves updated_by = the caller, byte-identical to the plain user-context write. Corroborated in two more places — sys_stamp_audit_update gates on session?.userId with no isSystem test, and plugin-audit's writeAudit reads sess.userId ?? provenance.attributedUserId with no isSystem gate.
The svc:flow: labelling the prose describes is real, but it is the user-less case — a schedule trigger, where there genuinely is no operator. The sentence generalises it to all runAs:'system' runs.
Why it is worth a card rather than a shrug
The prose reads as "system elevation costs you the operator in the audit trail."
That belief is load-bearing in the wrong direction. In the downstream app it was written into an adjudication as the explicit stop-condition for a security design — "if elevation erases the operator, stop and report a fork, because §九·58 requires 谁修改了什么…都要有日志可查". Had the investigating agent read the contract prose and trusted it, the correct design would have been abandoned on a false premise, and the card would have stopped one measurement short of the truth.
It did not, because it measured instead of reading. But the next reader may not, and a doc that talks someone out of the right answer is worth more than a cosmetic fix.
Say what is true of both cases — elevation decides authorization and leaves attribution alone; the svc:flow: actor label is what a genuinely user-less run (a schedule) falls back to.
Claim: downstream PM seat (repo:steedos-labs/hotcrm-heimao), dispatching under maintainer direct instruction
Session: session_01UHvF5hyiZjnCyExFnfQB8m
Branch: claude/issue-14011-stale-elevation-prose
Container & model: S, mode:subagent, model: opus (spec text face — per the model-tiering rule, spec prose is sonnet/opus; clause ② does not attach because no schema shape, acceptance set or public surface moves)
Clause-②: no
Maintainer instruction, verbatim:
上游需要解决的问题你也同步派发
⚠️Lane note, stated rather than assumed. This is a multi-lane repo and packages/spec belongs to the domain:spec seat. I am the downstream repo:* seat for steedos-labs/hotcrm-heimao, filing and dispatching here only because the maintainer directed it. Accordingly:
I have not set a domain:* label — that is the central triage seat's exclusive production, not mine to invent.
The PR will be left draft; the spec lane or the maintainer lands it. I do not self-merge in a lane I do not hold.
If the spec seat wants this back, take it — the claim is a courtesy, not a hold.
{
"issue": 14011,
"status": "done",
"branch": "claude/issue-14011-runas-attribution-prose",
"pr": "https://github.com/objectstack-ai/objectstack/pull/14035",
"premise_still_valid": true,
"summary": "Prose-only fix; runtime behaviour untouched. Located the source of the shipped sentence: packages/spec/src/contracts/automation-service.ts (the AutomationContext.flowName JSDoc), which publishes to dist/contracts/index.d.ts:3000 on this branch's 17.2.0 build (the card read :3033 on 17.1.0 -- same sentence, drifted line). Re-verified both cases in source at 62a137b rather than trusting the dispatch: resolveRunDataContext's runAs:'system' branch spreads the trigger's userId/tenantId through unchanged (#5494), objectql's sys_stamp_audit_insert/update stamp created_by/updated_by under a bare `if (session?.userId)` with no isSystem test, and plugin-audit's writeAudit computes actorLabel = userId ?? sess.actor -- so the svc:flow: label is the FALLBACK, and it is the genuinely user-less run (a schedule) that lands on it. Readings match the dispatch. Rewrote the JSDoc to be true of both cases. Swept packages/spec/src, all package JSDoc and content/docs for the same wrong concept (resolves no user / unattributed / user-less / Unknown user / elevation-vs-anonymity / every prose mention of runAs:'system' and svc:flow:): exactly ONE site carried the wrong generalisation, the one fixed; every other user-less mention is already correctly scoped and was left alone. Added packages/services/service-automation/src/runas-attribution-contract.test.ts, which asserts the invariant at the end of the chain (the session envelope the audit writers read) rather than at resolveRunDataContext's return shape, which builtin/crud-runas.test.ts already pins. TWO NOTES FOR PM. (1) The claim comment on the card names branch claude/issue-14011-stale-elevation-prose; the dispatch prompt names claude/issue-14011-runas-attribution-prose. I followed the dispatch prompt, so the PR is on the latter and the claim comment's branch does not exist -- a PM-side half-state, flagged not corrected. (2) The first ablation caught a phantom assertion in my own test: seeding the rows as the operator left updated_by already equal to the expected value, so the column check stayed green under mutation. Rows are now seeded by a different user (usr_creator) so the column must MOVE; re-ablated red on that assertion. No out-of-scope defects found worth a card. No domain:* label set and the PR is left draft, per the dispatch.",
"tests": "All readings from branch head aca37be (git rev-parse --short HEAD), gate union run AFTER the final commit. Heavy runs went through scripts/pm/os-verify-lock.sh; verdicts read from its VERDICT lines, exit codes captured before any pipe. (1) pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 -> 'Test Files 93 passed (93) / Tests 1099 passed (1099)', VERDICT command-exit 0 -- no existing behaviour assertion moved, which was the dispatch's blocked-condition. (2) pnpm --filter @objectstack/spec test -> 'Test Files 447 passed (447) / Tests 11991 passed (11991)', VERDICT command-exit 0. (3) pnpm --filter @objectstack/spec exec tsc --noEmit -> 0 errors. (4) pnpm --filter @objectstack/service-automation exec tsc --noEmit --listFiles -> exactly the 3 ledgered TS2341 in nested-region-parity.test.ts, 0 naming the new file; --listFiles grep confirms the new test file IS in that tsc program (the NOT-MEASURED trap), so the check-type-check-coverage DEBT entry (errors: 3) does not move. (5) pnpm lint (repo-wide eslint . --no-inline-config) -> exit 0, no findings; no narrowing needed. (6) Gate family derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (script derives its own change set from the merge base; --repo assertion held against this checkout's origin) -> 43 path-derived + convention-triggered families, harvested via --commands = 48 commands, each run with output redirected and $? captured immediately: 45 exit 0. The 3 non-zero are all exit 3 = PREREQUISITE NOT MET / NOT MEASURED, not findings -- check:type-check-debt, check:dual-build-cjs-loads and scripts/check-dev-prereqs.mjs each refuse without a fully built workspace closure (CI builds it first, as lint.yml does; the substance is covered by reading (4)); scripts/check-test-completeness.mjs needs a saved turbo test log it cannot produce; scripts/pm/check-half-states.mjs needs repo-scoped egress this container lacks (REST probe to api.github.com returned 403, gh absent). ABLATIONS: two, both against a COMMITTED tree, each with an absolute-path trap RESTORE_FN EXIT INT TERM. NO REBUILD WAS NEEDED and here is why: the mutated file, packages/services/service-automation/src/runtime-identity.ts, is reached from builtin/crud-nodes.ts by a relative source import inside its own package, and that package's vitest.config.ts aliases only @objectstack/platform-objects -- nothing routes its own modules through exports/dist, so vitest reads the mutated TypeScript source directly. Mutation confirmed ON DISK each time by grep -c of both the removed anchor and the injected text (anchor 1->0, inject 0->1) PLUS git hash-object against the HEAD blob (27a0301 -> eae3378 for A, -> 5841cf0 for B); restore leg proved by git checkout HEAD -- ABSOLUTE_PATH followed by hash-object equality with the HEAD blob and an empty git diff HEAD. Ablation A (deleted the userId carry-through, i.e. made the code match the OLD prose): 'AssertionError: the elevated run must stamp the triggering operator: expected usr_creator to be usr_operator', Test Files 1 failed (1) / Tests 1 failed | 1 passed (2) -- direction as predicted (operator limb red, user-less limb green). Ablation B (replaced the svc:flow: label with a constant, to prove the second limb is live): two AssertionErrors, 'expected svc:ABLATED to be svc:flow:elevated_touch' and 'expected svc:ABLATED to be svc:flow:night_sweep', Tests 2 failed (2). Guard green again after both restores. Also rebuilt packages/spec after the edit and confirmed the corrected sentence in dist/contracts/index.d.ts (1 occurrence) with the stale one gone (0).",
"mcp_calls": "5 -- issue_read get, issue_read get_comments, create_pull_request, pull_request_read get, add_issue_comment (this report). REST was probed first and refused: curl to api.github.com/repos/objectstack-ai/objectstack/pulls/14035 returned HTTP 403 and gh is absent, so MCP was the only write/read-back channel.",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
hotlong commentedon Aug 31, 2026
Claim: downstream PM seat (
repo:steedos-labs/hotcrm-heimao), dispatching under maintainer direct instructionSession:
session_01UHvF5hyiZjnCyExFnfQB8mBranch:
claude/issue-14011-stale-elevation-proseContainer & model: S,
mode:subagent,model: opus(spec text face — per the model-tiering rule, spec prose is sonnet/opus; clause ② does not attach because no schema shape, acceptance set or public surface moves)Clause-②: no
Maintainer instruction, verbatim:
packages/specbelongs to thedomain:specseat. I am the downstreamrepo:*seat forsteedos-labs/hotcrm-heimao, filing and dispatching here only because the maintainer directed it. Accordingly:domain:*label — that is the central triage seat's exclusive production, not mine to invent.If the spec seat wants this back, take it — the claim is a courtesy, not a hold.
Generated by Claude Code
docs(spec): correct AutomationContext.flowName's attribution prose (#…
hotlong commentedon Sep 1, 2026
os-dev-report
{ "issue": 14011, "status": "done", "branch": "claude/issue-14011-runas-attribution-prose", "pr": "https://github.com/objectstack-ai/objectstack/pull/14035", "premise_still_valid": true, "summary": "Prose-only fix; runtime behaviour untouched. Located the source of the shipped sentence: packages/spec/src/contracts/automation-service.ts (the AutomationContext.flowName JSDoc), which publishes to dist/contracts/index.d.ts:3000 on this branch's 17.2.0 build (the card read :3033 on 17.1.0 -- same sentence, drifted line). Re-verified both cases in source at 62a137b rather than trusting the dispatch: resolveRunDataContext's runAs:'system' branch spreads the trigger's userId/tenantId through unchanged (#5494), objectql's sys_stamp_audit_insert/update stamp created_by/updated_by under a bare `if (session?.userId)` with no isSystem test, and plugin-audit's writeAudit computes actorLabel = userId ?? sess.actor -- so the svc:flow: label is the FALLBACK, and it is the genuinely user-less run (a schedule) that lands on it. Readings match the dispatch. Rewrote the JSDoc to be true of both cases. Swept packages/spec/src, all package JSDoc and content/docs for the same wrong concept (resolves no user / unattributed / user-less / Unknown user / elevation-vs-anonymity / every prose mention of runAs:'system' and svc:flow:): exactly ONE site carried the wrong generalisation, the one fixed; every other user-less mention is already correctly scoped and was left alone. Added packages/services/service-automation/src/runas-attribution-contract.test.ts, which asserts the invariant at the end of the chain (the session envelope the audit writers read) rather than at resolveRunDataContext's return shape, which builtin/crud-runas.test.ts already pins. TWO NOTES FOR PM. (1) The claim comment on the card names branch claude/issue-14011-stale-elevation-prose; the dispatch prompt names claude/issue-14011-runas-attribution-prose. I followed the dispatch prompt, so the PR is on the latter and the claim comment's branch does not exist -- a PM-side half-state, flagged not corrected. (2) The first ablation caught a phantom assertion in my own test: seeding the rows as the operator left updated_by already equal to the expected value, so the column check stayed green under mutation. Rows are now seeded by a different user (usr_creator) so the column must MOVE; re-ablated red on that assertion. No out-of-scope defects found worth a card. No domain:* label set and the PR is left draft, per the dispatch.", "tests": "All readings from branch head aca37be (git rev-parse --short HEAD), gate union run AFTER the final commit. Heavy runs went through scripts/pm/os-verify-lock.sh; verdicts read from its VERDICT lines, exit codes captured before any pipe. (1) pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 -> 'Test Files 93 passed (93) / Tests 1099 passed (1099)', VERDICT command-exit 0 -- no existing behaviour assertion moved, which was the dispatch's blocked-condition. (2) pnpm --filter @objectstack/spec test -> 'Test Files 447 passed (447) / Tests 11991 passed (11991)', VERDICT command-exit 0. (3) pnpm --filter @objectstack/spec exec tsc --noEmit -> 0 errors. (4) pnpm --filter @objectstack/service-automation exec tsc --noEmit --listFiles -> exactly the 3 ledgered TS2341 in nested-region-parity.test.ts, 0 naming the new file; --listFiles grep confirms the new test file IS in that tsc program (the NOT-MEASURED trap), so the check-type-check-coverage DEBT entry (errors: 3) does not move. (5) pnpm lint (repo-wide eslint . --no-inline-config) -> exit 0, no findings; no narrowing needed. (6) Gate family derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (script derives its own change set from the merge base; --repo assertion held against this checkout's origin) -> 43 path-derived + convention-triggered families, harvested via --commands = 48 commands, each run with output redirected and $? captured immediately: 45 exit 0. The 3 non-zero are all exit 3 = PREREQUISITE NOT MET / NOT MEASURED, not findings -- check:type-check-debt, check:dual-build-cjs-loads and scripts/check-dev-prereqs.mjs each refuse without a fully built workspace closure (CI builds it first, as lint.yml does; the substance is covered by reading (4)); scripts/check-test-completeness.mjs needs a saved turbo test log it cannot produce; scripts/pm/check-half-states.mjs needs repo-scoped egress this container lacks (REST probe to api.github.com returned 403, gh absent). ABLATIONS: two, both against a COMMITTED tree, each with an absolute-path trap RESTORE_FN EXIT INT TERM. NO REBUILD WAS NEEDED and here is why: the mutated file, packages/services/service-automation/src/runtime-identity.ts, is reached from builtin/crud-nodes.ts by a relative source import inside its own package, and that package's vitest.config.ts aliases only @objectstack/platform-objects -- nothing routes its own modules through exports/dist, so vitest reads the mutated TypeScript source directly. Mutation confirmed ON DISK each time by grep -c of both the removed anchor and the injected text (anchor 1->0, inject 0->1) PLUS git hash-object against the HEAD blob (27a0301 -> eae3378 for A, -> 5841cf0 for B); restore leg proved by git checkout HEAD -- ABSOLUTE_PATH followed by hash-object equality with the HEAD blob and an empty git diff HEAD. Ablation A (deleted the userId carry-through, i.e. made the code match the OLD prose): 'AssertionError: the elevated run must stamp the triggering operator: expected usr_creator to be usr_operator', Test Files 1 failed (1) / Tests 1 failed | 1 passed (2) -- direction as predicted (operator limb red, user-less limb green). Ablation B (replaced the svc:flow: label with a constant, to prove the second limb is live): two AssertionErrors, 'expected svc:ABLATED to be svc:flow:elevated_touch' and 'expected svc:ABLATED to be svc:flow:night_sweep', Tests 2 failed (2). Guard green again after both restores. Also rebuilt packages/spec after the edit and confirmed the corrected sentence in dist/contracts/index.d.ts (1 occurrence) with the stale one gone (0).", "mcp_calls": "5 -- issue_read get, issue_read get_comments, create_pull_request, pull_request_read get, add_issue_comment (this report). REST was probed first and refused: curl to api.github.com/repos/objectstack-ai/objectstack/pulls/14035 returned HTTP 403 and gh is absent, so MCP was the only write/read-back channel.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
hotlong commentedon Sep 1, 2026
更正认领评论的分支名 —— 是我(PM)写岔了,dev 报了上来。
claude/issue-14011-stale-elevation-prose❌ 不存在claude/issue-14011-runas-attribution-prose✅dev 按派发词走(对的),所以 PR #14035 在后者上。以本条为准。
这个错值得记一句而不是悄悄改:所有 agent 共用一个 GitHub 身份,认领评论里的分支名就是身份位 —— 它是别人判断「这张卡有没有人在做、做在哪」的唯一凭据。指向一个不存在的分支,等于把这张卡显示成无人认领,而实际上有人正在上面工作。两个 agent 同时开工的事故就是这么来的。
派发词与认领评论是我在两次编辑里写的,名字在中间漂了。教训:认领评论与派发词的分支名应当从同一处复制,不重打一遍。
dev 的处理也是对的 —— 报上来,没有自作主张去改我的评论(共享身份下,改别人的写入是禁形)。
Generated by Claude Code
docs(spec): correct AutomationContext.flowName's attribution prose (o…