Skip to content

fix(runtime): a per-record action called with no recordId is refused 400 naming it, and an unknown id 404s at the door - #29976

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-mtl298
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-mtl298

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #29881

维护者速读

Clause-②: no
Metadata-protocol: none

What changes

Why

Card #29881: POST /api/v1/actions/showcase_task/showcase_recalc_estimate {} answered 400 FLOW_FAILED with "Node 'recalc_each' failed: … Node 'load_task' failed: get_record: refusing to run — 1 filter operand(s) evaluated to nothing: recordId…". With an unknown id it answered the same code wrapping "Record … not found". The caller could not tell that it had left out an argument, and the flow's internal node names reached it.

Premise readings (measured at 18112bfa):

Accept set (Clause-②: no, the #29005 reading). Every call refused now already failed on main. With no id, the forward ran a flow that refused at its first read. With an unresolvable id, it ran a flow that could not read the row as the caller (runAs: 'user'). In-repo census of execution: 'perRecord': four showcase actions. Two are operation: 'update' (that arm is untouched). Two are api actions: the recalc one, and showcase_generate_invoice_pdf → /api/v1/print/records, whose route reads the record as the caller. No plugin package declares perRecord.

Verification

  • Unit, packages/runtime/src/domains/api-action-forward.test.ts, new [#29881] describe, through the real HttpDispatcher REST door and invokeBusinessAction (MCP):
    • with {}, { params: {} } and { recordId: '' }, the request is refused 400 VALIDATION_ERROR naming recordId, and the target is never called;
    • an unresolvable id gets 404 RECORD_NOT_FOUND, and nothing is forwarded;
    • controls: a real id is forwarded with { recordId }, and the aggregate twin with no id is forwarded;
    • helpers: forwardsOnDeniedSubjectLoad is false for per-record and true for aggregate and undeclared; buildApiActionRequest builds no request without an id.
  • Unit, packages/runtime/src/action-flow-per-record-record-id.test.ts (new): a type: 'flow' per-record action, on both doors, is refused 400 naming recordId, and automation.execute is never called. An unknown id keeps the 404. A real id runs. Aggregate and undeclared flow actions run with no id.
  • Real HTTP, packages/qa/dogfood/test/api-action-door-forward.dogfood.test.ts, new [#29881] case on a booted stock showcase, as admin:
    • showcase_recalc_estimate with {} and with { recordId: '' } answers 400 VALIDATION_ERROR naming recordId; the body contains neither load_task nor FLOW_FAILED;
    • an unknown id answers 404 RECORD_NOT_FOUND with no load_task;
    • a seeded task answers 200;
    • showcase_recalc_selection {} is not refused at the door.
    • The file has 11 tests, all passed.
  • Ablations, each run once and restored:
    1. Unit: the three call sites removed (helper kept so the import resolves): 6 failed, every new negative pin, e.g. REST: no recordId … answers 400 VALIDATION_ERROR → AssertionError: {}: expected 200 to be 400, and the 404 pin → expected 200 to be 404. Restored: 92 passed.
    2. Real HTTP: runtime rebuilt from main's action-execution.ts. The [#29881] case failed with the card's body verbatim: {"code":"FLOW_FAILED","message":"Node 'recalc_each' failed: map 'recalc_each': item 0 (subflow 'showcase_recalc_task_estimate') failed: Node 'load_task' failed: get_record: refusing to run — 1 filter operand(s) evaluated to nothing: \recordId` …"}: expected 'FLOW_FAILED' to be 'VALIDATION_ERROR'`. Rebuilt with the fix: 11 passed.
  • Build and suites: the @objectstack/runtime... closure build: exit 0. The @objectstack/dogfood^... closure build: exit 0. Runtime pnpm test: 540 files, 8614 passed / 19 skipped, exit 0. test:repo: 3 files, 1211 passed, exit 0. tsc --noEmit: exit 0. check:test-typecheck: exit 0.
  • pnpm lint: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands at the change set vs origin/main 18112bfa: 64 derived commands, all exit 0, with two notes:
    • check-plugin-teardown-shape.mjs --self-test first exited 3 because its fixture commit 621a4876 was not in the shallow clone; after fetching it: 48 cases pass, exit 0.
    • check:dual-build-cjs-loads first exited 3 because 7 package dists were missing; after building them: exit 0 (116 entry points, 71 packages).
    • The 5 artifact-roster gates whose roster sits under a path this diff touches (check:authz-resolver, check:changeset-fixed, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census): all exit 0.
    • dispatch-gates --ran: ✓ 64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED.
    • The gates ran on commit 63068066. The pushed head 3648acbb is the same tree: only the commit message was reworded, for the trailer hook.
  • Serial: no open PR found touching the action door's per-record path. The one open PR a search returned (fix(core): a successful operation: 'update' action refreshes the record it wrote #29970) edits only ui/ files.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (spec untouched)
  • 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: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 section is the seat's Chinese summary for the maintainer).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzf1rWiBfPzSuFgeCcr

…400 naming it, and an unknown id 404s at the door

An action declared execution: 'perRecord' (api or flow) called with no
recordId is refused 400 VALIDATION_ERROR naming it before anything is
forwarded or started, through both action doors. The same api action with
an id the caller's scope does not resolve answers the door's 404
RECORD_NOT_FOUND instead of being forwarded. Aggregate and undeclared
actions run record-less as before (#29173).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzf1rWiBfPzSuFgeCcr
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 3648acbb91dacdc4c061dd6f330be1434725a1c0
Local-runs: none

Face: declaration (Clause-②: no, Metadata-protocol: none). No published schema and no governed path in the committed tree (5 files). Read: the full diff, card #29881 and its triage audit 6600572196, the dev report (/mnt/project-files/issue-29881/os-dev-report.md), ActionSchema.execution (packages/spec/src/ui/action.zod.ts), and #29005 (#28932's fix).

① Derived judgments

② Semver level

'@objectstack/runtime': patch. perRecordActionRecordIdRefusal is exported from action-execution.ts; the dev reports no entry-export change, so the package surface does not move. Consistent with both declaration lines.

③ Boundary flags

  • No open_questions. An out-of-repo action declared perRecord whose body happens not to read its record, called with no id, is now refused where it ran before. That is the declared contract being enforced (an author whose body serves both omits execution). The changeset names the rule. Not filed.

Implemented-by: claude/project-thread-mtl298
Reviewed-by: session_01WGufzibcN9oHa8kDbTTnL5

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

席位核对

Verdict: ACCEPT at head 3648acbb91dacdc4c061dd6f330be1434725a1c0 · 2026-11-03T21:10Z

  • Shape: 5 files: a runtime action-door helper read by the api and flow arms, the denied-load forward exemption, unit and forward tests, the dogfood door test, and one patch changeset. No published schema, no governed path ⇒ not Tier H. TIER_DEFAULT review PASS 6615520067 at this head.
  • Premise measured: the dev reproduced the card's FLOW_FAILED body through the dogfood door test (ablation). The mechanism differs from the card: these are api actions forwarding to a flow, so the fix reads the action's execution: 'perRecord'.
  • Gates: dispatch-gates 64/64 exit 0, --ran 0 NOT-MEASURED. Runtime unit, forward and dogfood suites pass. Ablations went red and were restored. Landing waits for every check on this head to be green or an expected skip.
  • Accepted deviations: the harness-pinned branch claude/project-thread-mtl298. The landing site moved from the token check to the execution declaration, measured. The signed trailer is appended by the seat through the relay.
  • Landing: on all-green at this head, the seat sends pr_ready + automerge_enable through the relay.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 5 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ui/actions.mdx (via dispatchFlowAction (symbol, a top-level function), perRecord (literal, a string literal in a comment on a changed line; a string literal in forwardsOnDeniedSubjectLoad; a string literal in perRecordActionRecordIdRefusal))
  • content/docs/ui/views.mdx (via perRecord (literal, a string literal in a comment on a changed line; a string literal in forwardsOnDeniedSubjectLoad; a string literal in perRecordActionRecordIdRefusal))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-5.mdx (via perRecord (literal, a string literal in a comment on a changed line; a string literal in forwardsOnDeniedSubjectLoad; a string literal in perRecordActionRecordIdRefusal))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 18112bfa776b896f705c182f07b8baeff45957a7 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 18112bfa776b896f705c182f07b8baeff45957a7

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 18112bfa776b896f705c182f07b8baeff45957a7 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…, not run record-less

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzf1rWiBfPzSuFgeCcr
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

席位核对

Verdict: ACCEPT at head e2aec5f47e559fbdcff590526fe1c0414d809f94 · 2026-11-03T21:22Z

  • Supersedes ACCEPT 6615564413 at 3648acbb. The head moved by one dev docs commit, prose only (head-move exception: prose surface, original record carries on): content/docs/ui/actions.mdx gains one sentence saying a perRecord api or flow action with no recordId is refused 400 VALIDATION_ERROR and an unresolved record answers 404 RECORD_NOT_FOUND. That matches the behaviour judged in review PASS 6615520067. No code, test or changeset moved. The 26 docs commands dispatch-gates derived for the new change set exit 0.
  • Landing: on all-green at this head, the seat sends pr_ready + automerge_enable through the relay.

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

1 participant