Skip to content

fix(runtime): the /automation definition doors record which user saved a flow - #27538

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-27414-flow-save-actor
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-27414-flow-save-actor

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #27414. This PR delivers the sys_metadata half: the row's created_by / updated_by and its history's recorded_by. The sys_audit_log half is not in this PR. Its null user_id is produced in packages/metadata-protocol, outside this card's claimed file surface, and the same null appears on the /meta door (measured below). The report on the card says how to finish it.

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么: 管理员通过 POST /api/v1/automation(新建)、PUT /api/v1/automation/:name(更新)、POST /api/v1/automation/:name/clone(克隆)保存 flow 时,sys_metadata 行的 created_by / updated_by 和历史行的 recorded_by 现在记的是这位管理员,不再是空。
  • 为什么改: 三个门共用 registerAndSaveFlow,它调 saveMetaItem 时没带 actor。/meta 门一直带着,所以同一个 flow 从 /meta 存能记人,从 /automation 存就记不到。ADR-0073 D3 要求归属必须具体。
  • 风险与回滚: 只多传一个用户 id,取自已鉴权的执行上下文,不读请求体;无用户的系统调用照旧不带 actor。回滚 = revert 本 PR。未解决:同一写入的 sys_audit_log 行 user_id 仍为空,/meta 门也一样,根因在 packages/metadata-protocol,不在本卡文件面内。
  • 先看: packages/runtime/src/domains/automation.ts 的 registerAndSaveFlow(saveMetaItem 那一行);packages/runtime/src/domains/automation-flow-save-actor.test.ts。
  • 两分钟验证: pnpm --filter @objectstack/runtime exec vitest run src/domains/automation-flow-save-actor.test.ts,预期 3/3 通过:新建、更新、克隆的行都写着调用者 id,无用户的对照写 null。
  • 需要你做的: 无,席位落地。

What changes

  • registerAndSaveFlow in packages/runtime/src/domains/automation.ts is the one persistence path for POST /automation, PUT /automation/:name and POST /automation/:name/clone. It now passes the caller's user id as saveMetaItem's actor. The id is read through activationCaller(context), the helper the toggle door uses since PR fix(service-automation): the flow toggle records which admin switched a packaged flow #26605: the signed-in user's id from the execution context and nothing else, never from the body. A context with no user passes no actor, so a system write stays unattributed as before.
  • The three callers already passed context into the function, so no caller changed.
  • automation-authoring-doors-durable.test.ts: two exact-argument pins on saveMetaItem now include actor: 'u_admin', the id of the context those tests already send.

Tests

Measured before the change, on the stock showcase (fresh file DB, signed in as admin@objectos.ai), at base ded67bf

The three /automation doors and, as a control, PUT /api/v1/meta/flow/:name. Then each new sys_metadata row and the sys_audit_log rows on sys_metadata:

row qa_clone_before  created_by None updated_by None
row qa_create_before created_by None updated_by None      (created by POST, then updated by PUT)
row qa_meta_before   created_by O9Nq...BM updated_by O9Nq...BM   (control: the /meta door)
audit create sys_metadata (meta row)   user_id None actor None
audit update sys_metadata (create row) user_id None actor None
audit create sys_metadata (create row) user_id None actor None
audit create sys_metadata (clone row)  user_id None actor None

Create and update show the same null actor as clone, so all three are in scope (same function, same fix). The audit rows are null on all four doors, including the /meta door that already passes actor.

Measured after the change, same rig, runtime rebuilt (head d325afa)
row qa_clone_after  created_by LzwM...1y updated_by LzwM...1y
row qa_create_after created_by LzwM...1y updated_by LzwM...1y
row qa_meta_after   created_by LzwM...1y updated_by LzwM...1y   (control, unchanged)
audit create/update sys_metadata, all four rows: user_id None actor None   (not changed by this PR)
  • New packages/runtime/src/domains/automation-flow-save-actor.test.ts (3 cases). It uses a real store, the same rig as meta-save-actor.test.ts: better-sqlite3 :memory:, the real sys_metadata* objects and a real ObjectStackProtocolImplementation. The door is driven through the real HttpDispatcher, and the rows are read back. Create records the creator, update by another user records that user as updater, clone records the cloning admin. The control is a caller with no user: its rows record null on all three doors.
Ablation (one mutation, through scripts/ablation-replace.mjs)
  • The fix was committed first. Mutation: saveMetaItem's argument lost the actor spread. Anchor 1 to 0, blob 14a061fa to 540e6524.
  • Result: 4 failed, 10 passed. The failures were the 2 durable pins and the create/update and clone row pins. The no-user control stayed green.
  • Restore: blob back to HEAD (14a061fa), git diff HEAD empty.
  • The runtime suite imports automation.ts from src (relative import), so no rebuild was needed to observe the mutation.
  • Package suites at head 8551475, under the verify lock: pnpm --filter @objectstack/runtime typecheck exit 0, including its test-layer check. pnpm --filter @objectstack/runtime test: 472 files passed, 7671 tests passed, 19 skipped.
  • After merging origin/main (head f1f7ce3; incoming commits touch objectql, so its closure was rebuilt): 6 related files re-run (the new pin, durable, tenant-authored-write, flow-clone, meta-save-actor, toggle-caller), 47/47 passed.

Acceptance notes

  • Not delivered: the sys_audit_log half of automation: cloning a packaged flow writes the sys_metadata row and its audit row with a null actor, so nothing records which admin created the clone #27414. SysMetadataRepository.put in packages/metadata-protocol/src/sys-metadata-repository.ts writes the sys_metadata row under { ...ctx, isSystem: true }, with no session.userId and no provenance.attributedUserId. The audit writer in plugin-audit takes user_id from exactly those two fields, so the audit row names nobody on every metadata save door. The /meta door is included, and the live control above shows it. The actor reaches updated_by / created_by / recorded_by but never the engine write's context. That file is outside this card's claimed surface, so it is left to the seat.
  • The toggle doors record the user on their audit rows because their writes run with the caller's id on the context. That is the activation ledger, a different write path.

Verification

  • Gate list derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on this diff: 61 commands. Each exit code was captured before any pipe. --ran reconciliation: 61 derived, 59 run, 2 NOT-MEASURED, 0 UNRUN.
  • NOT MEASURED, prerequisite not met (exit 3), left to CI: node scripts/check-plugin-teardown-shape.mjs --self-test (shallow clone, its pinned control commit is unreachable; the scan itself exited 0) and pnpm check:dual-build-cjs-loads (needs a full workspace build).
  • eslint --no-inline-config on the three touched .ts files: exit 0. The repo-wide lint is CI's.
  • Gates were run at 8551475. The later merge of origin/main brought no change to packages/runtime or to anything these gates derive from.

Generated by Claude Code

…s user id

POST /automation, PUT /automation/:name and POST /automation/:name/clone
persist through registerAndSaveFlow, which called saveMetaItem with no
actor, so the sys_metadata row and its history named nobody. The save
now passes the caller's id, read through the toggle door's
activationCaller, and nothing when the context has no user.

Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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 58 of 210 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 — 28 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 aef631b80b3175f2f43287a6e0185c828c356ac8 → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

席位核对

Verdict: ACCEPT (partial delivery) at head f1f7ce31ee3adc4e2f74dfb574a35f0db76e4792, reviewed 2026-10-29T11:46Z. Landing waits for CI on this head to finish green.

check reading
Shape Draft, base main. The body opens Part of #27414, and no closing keyword appears anywhere in it (checked). That is correct: one item of #27414 is delivered here and the other is #27539.
Path surface 4 files under packages/runtime plus a changeset, none governed. No contract face: no spec, no governed text, no accept set narrowed.
Clause-② no, correct.
Changeset @objectstack/runtime patch. Each sentence matches the diff, including the honest last line that sys_audit_log is not changed here.
Diff read registerAndSaveFlow passes actor from activationCaller(context), the #26605 helper, so the three /automation doors and the toggles read the caller one way. A context with no user passes no actor.
Assertions A real-store pin covers create plus update by a second user, clone, and a no-user control on all three doors, reading the rows back. The two durable pins now expect actor. The ablation turns 4 cases red with the control green.
Dev evidence At 85514752 under the lock, the runtime suite passed 7671; after the main merge (f1f7ce31), the 6 related files passed 47/47. dispatch-gates --ran: 61 derived, 59 run, 2 NOT MEASURED for environment reasons (CI runs them). mcp_calls 0.

Open question from the dev (how to finish the sys_audit_log half): option A, shaped as the protocol's in-flight sub-issue. It is #27539, a sub-issue of #27414 claimed by this seat and dispatched on claude/issue-27539-metadata-save-audit-actor, with the surface packages/metadata-protocol/src/sys-metadata-repository.ts reusing the audit writer's provenance.attributedUserId channel. #27414 stays open until #27539 lands.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

Ready to land at 2026-10-29T11:54Z, held. CI on head f1f7ce31 is complete: 48 check-runs, 34 success, 14 skipped. check-expected-skips --pr 27538 is OK (all 14 skips are in main's roster). check-governed-merges --pr 27538: 0 of 4 paths governed, 169 lines.

The landing step (relay pr_ready + automerge_enable) is the command this session's permission classifier refused on #27424 (comment 6494048067), so it is not run here. Needed from the maintainer: allow that command for this seat, or mark this PR ready and enable auto-merge by hand. On merge, #27414 stays open for #27539 (Part of, no closing keyword).


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 16:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 33ab543 Oct 29, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-27414-flow-save-actor branch October 29, 2026 17: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

Development

Successfully merging this pull request may close these issues.

2 participants