Skip to content

runtime: two hand-built 400 refusals put off-catalog codes on details.fields[].code (unrecognized_keys, invalid_string), which ADR-0114's closed FieldErrorCode catalog forbids #24597

Closed

Description

@objectstack-fleet

Filing gate ①, class (b): a published closed catalog that a public door's answer breaks, measured. reach: an operator's POST /api/v1/automation/approval_flow/runs/run_7/cancel with body {"foo":1} answers 400 VALIDATION_FAILED with details.fields[0].code: "unrecognized_keys". That was measured through the route's domain handler by #24546's dev (report 6271088846, PR #24594). The sites are in packages/runtime, which is domain:cli by the lane table.

Filed by domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T, from that report's out_of_scope_findings[0]. ⛔ Not a claim. Grade, lane and area:* are triage's. Reader who acts: the domain:cli seat.

The contract

  • packages/spec/src/api/errors.zod.ts (FieldErrorCode, about :222–:292 on origin/main 5a871e4e4) declares a closed catalog: "a service does not get to invent one — it adds a member here" (ADR-0114 D2).
  • The members for these two conditions are unknown_field ("a key the target does not declare"), which the resume door one route over already uses, and invalid_format ("a declared pattern/format the value does not match").
  • The published ApiErrorSchema types details loosely, so no gate sees an off-catalog code. A client that switches on the catalog gets a value it cannot name.

The sites (read on origin/main 5a871e4e4)

  • packages/runtime/src/domains/automation.ts:959: the run-lifecycle body refusal (cancel / restore-suspension unknown key) writes code: 'unrecognized_keys' as const, Zod's issue code.
  • packages/runtime/src/domains/permission-set-clone.ts:194: the clone door's name rule writes code: 'invalid_string' (Zod's), where the catalog has invalid_format.
  • Census: git grep -n "'unrecognized_keys'\|'invalid_string'" -- 'packages/runtime/src/**/*.ts' ':!*.test.ts' finds these two writers and one reader, packages/runtime/src/domains/packages.ts:888. That reader compares a Zod issue's own code, which is correct and not a wire write.
  • The done criterion below asks for the full census of hand-built details.fields writers against the catalog, not only these two.

Precedent

#8124 (closed) removed the same leak from fieldsFromZodIssues (ADR-0114 D3). These two sites build their field rows by hand, so that fix did not reach them.

What done looks like

  • Both sites write catalog members: unknown_field and invalid_format. The message text stays.
  • A census of every hand-built details.fields[] writer in packages/runtime, each checked against FieldErrorCode. Every off-catalog one is fixed in the same PR, or listed with its reason.
  • A pin per site: the refusal's details.fields[].code parses under FieldErrorCodeSchema, with an ablation.
  • Whether changing a wire code narrows anything is the PR's call to state (Clause-② line). No accept set moves; only the refusal's vocabulary does.

Dedupe


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p2 · bug · domain:cli · area:api · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-19T16:57Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on origin/main c03b17d784.

    • The premise holds: packages/runtime/src/domains/automation.ts:959 hand-builds code: 'unrecognized_keys'. The closed FieldErrorCode catalog is at packages/spec/src/api/errors.zod.ts:244.
    • p2: a door's answer that breaks a published closed catalog (ADR-0114 D2), at an operator door. The fix makes the door conform to what spec already declares (unknown_field and the catalog's format member). Nothing spec declares moves, so this is not in the protocol-first class.
    • Lane domain:cli (packages/runtime). Pin both sites with the catalog member, plus a closed-set check over the domain's hand-built refusals.
    • The bug label is kept.

    Generated by Claude Code

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    and removed on Oct 19, 2026
  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 6 · domain:cli seat 1 · p2 · 2026-10-19T22:25Z
    Session: session_01SYEJqDL3NXxrJRBVqaMu9T
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24597-field-code-catalog
    Worktree: objectstack-issue-24597
    Domain: domain:cli
    Seat: domain:cli#1 (seat post #6024)
    File surface (read on origin/main bfc1539fe; line numbers move; stop on a breach and explain it in the report):


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24597,
    "status": "done",
    "branch": "claude/issue-24597-field-code-catalog",
    "pr": "#24683",
    "session": "session_01SYEJqDL3NXxrJRBVqaMu9T — mode:subagent, the parent seat's harness-stamped id (the relay route read it from the container's CLAUDE_CODE_REMOTE_SESSION_ID; it is also the Claude-Session trailer on 5fd5f5e)",
    "premise_still_valid": true,
    "summary": "The premise held at origin/main bfc1539. automation.ts:957 (refuseInvalidRunLifecycleBody, cancel / restore-suspension unknown key) wrote unrecognized_keys, and permission-set-clone.ts:194 (readCloneBody name rule) wrote invalid_string. Both now write catalog members, unknown_field and invalid_format, each with a one-line ADR-0114 D2 comment. Measured through the real HttpDispatcher handler, a field-by-field diff of the before and after envelopes shows exactly one path changing per site, body.error.details.fields[0].code; status 400, error.code VALIDATION_FAILED, message, httpStatus and the row's field and message are byte-identical. Census: an AST walk of packages/runtime/src (tests excluded) finds 31 hand-built rows {field, code} and 48 helper calls. The two above were the only off-catalog rows; the other 29 are catalog members. endpoint-executor.ts:386 (required) is already pinned exactly in endpoint-executor.test.ts, and query-param.ts:81 takes a code parameter typed FieldErrorCode, so tsc closes it. Not wire writers: the fieldsFromZodIssues rows (ADR-0114 D3 mapper), the two empty-row refusals in data.ts, and the relays of producer rows (actions.ts:913, flowDefinitionRefusal's pass-through, the sandbox). packages.ts:888 compares a Zod issue's own code to name misplaced install options, a read and not a wire write. New pin file packages/runtime/src/domains/field-error-code-catalog.test.ts has 41 tests. Part 1 (5 tests) pins each site: the 400 VALIDATION_FAILED envelope, exact {field, code} rows, each code parsing under FieldErrorCode, and no service call. Part 2 (36 tests) is the closed-set check: it drives every hand-built row under src/domains/ through the real dispatcher and parses each details.fields[].code under FieldErrorCode, naming the row's expected {field, code}. No gate script or lint rule was added. The changeset is a patch on @objectstack/runtime with a line-initial Clause-②: no, and it names the wire change. The PR body carries Fixes #24597, the Clause-② line and its basis. Naming: the dispatch and claim say FieldErrorCodeSchema, but the spec exports the Zod enum as FieldErrorCode and no FieldErrorCodeSchema exists, so the pins parse under FieldErrorCode. Conflict, stated rather than resolved silently: os-dev.md makes repo-wide scans (pnpm lint) and test:repo CI-owned and never owed by the dev, while the dispatch asked for both. I ran both as asked (green); no standard clause forbids running them. The worktree was removed after the PR opened, without --force.",
    "tests": "All at 5fd5f5e (the final and only commit), each through scripts/pm/os-verify-lock.sh (VERDICT command-exit lines quoted). (1) New file: pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/domains/field-error-code-catalog.test.ts, 41 passed. (2) Full pnpm --filter @objectstack/runtime test (vitest project local): 'Test Files 426 passed (426) · Tests 6712 passed | 19 skipped (6731)', VERDICT command-exit 0, held 545s (a shared-box reading). (3) pnpm --filter @objectstack/runtime test:repo (vitest project repo): 'Test Files 3 passed (3) · Tests 808 passed (808)', VERDICT command-exit 0. (4) pnpm --filter @objectstack/runtime typecheck (tsc --noEmit, then check:test-typecheck over tsconfig.test.json): VERDICT command-exit 0. 'check:test-typecheck: OK — 27 file(s) / 190 error(s) / 68 pinned signature(s) held'. The new file has no ledger entry, so it compiles with zero errors. (5) Before and after envelopes, measured through handleAutomation / handleSecurity by a scratch vitest printer that was deleted and never committed. Before: unrecognized_keys on cancel and restore-suspension, invalid_string on clone. After: unknown_field and invalid_format. The diff is one path per envelope. (6) Ablation, one leg per fixed site, after the fix was committed. The test imports ../http-dispatcher.js relatively, so the subject resolves to src/ and no dist/ build is in the path. The mutation went through node scripts/ablation-replace.mjs inside a wrapper with a trap restore EXIT INT TERM on absolute paths. automation leg: code: 'unknown_field' as const, became code: 'unrecognized_keys' as const,; anchor 1 to 0, replacement 0 to 1, blob 84d4f8b34e42 to 5ad01f696481; result '4 failed | 37 passed' (both Part 1 run-lifecycle pins and both Part 2 run-lifecycle unknown-key rows, each failing with: details.fields[].code "unrecognized_keys" ... is not a FieldErrorCode member); restored blob 84d4f8b34e42 == HEAD, git diff HEAD empty. clone leg: code: 'invalid_format' became code: 'invalid_string'; anchor 1 to 0, blob 820646d40ba1 to ffdb9582fd37; result '4 failed | 37 passed' (three Part 1 clone pins and the Part 2 name-rule row, same catalog-parse assertion); restored blob 820646d40ba1 == HEAD, git diff HEAD empty. Direction: red, as predicted; no other test moved. (7) Grep of other packages' tests for assertions on unrecognized_keys / invalid_string from these two doors: none. Every hit reads a Zod issue's own code on a local parse (cli, lint, metadata-protocol, drivers, core logger, service-automation wait-node). In ../objectui (79cb8c0), clientValidation.ts:1020 reads a client-side Zod issue code, not these doors' wire rows.",
    "gates": "Local subset, at 5fd5f5e: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 67 commands (4 changed paths against merge base bfc1539; 365 changed lines, under the 3000 threshold). Each ran byte for byte with its exit code captured before any pipe. First pass: 66 exit 0, and pnpm check:dual-build-cjs-loads exited 3 ('PREREQUISITE NOT MET — this gate reads built output', 39 packages without dist/). After a full turbo run build --filter=!@objectstack/docs --concurrency=2 (VERDICT command-exit 0, 76 tasks, 75 cached) it exited 0: '111 published require entry point(s) across 69 package(s) load'. check:dts-closure (76 packages swept, against 32 before), check:sourcemap-no-sources-content, check:lean-entry-closure and check:published-files were re-run on the built tree, all exit 0. Reconciliation: node scripts/pm/dispatch-gates.mjs --ran ran.list, exit 0, 'Run reconciliation — 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.' Full union, as the dispatch asked: pnpm lint (eslint . --no-inline-config), LINT_EXIT=0, no output. check-platform-checklist --changed-from does not apply, because no exported symbol was renamed or removed. CI on PR #24683 at head 5fd5f5e, read when this report was written (37 check-runs), was not waited on. success: Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter. skipped: Build Docs, Console Pin Gate, Dogfood Regression Gate (shard matrix), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL). in_progress: Build Core, Check Changeset, Governed Surface Queue Guard, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Test Core (1/6) through (6/6), Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace.",
    "mcp_calls": "0 — no MCP GitHub tool was called. Reads went through single-card REST GETs (gh api) on the card, its two comments, PR #24683 and its check-runs.",
    "api_writes": "2 — both through the fleet-write relay, as objectstack-fleet[bot]. Each is one POST /repos/objectstack-ai/objectstack/dispatches. (1) pr_create, which executed POST /repos/objectstack-ai/objectstack/pulls (draft, #24683; read-back: 15814 bytes sent, 15814 stored, identical) and POST /repos//issues/24683/assignees (zhuangjianguo, listed). (2) this os-dev-report comment, which executed POST /repos//issues/24597/comments. Labels: none sent; skip-changeset does not apply because a patch changeset ships. git push (2, not REST): the empty-branch probe and 5fd5f5e.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none (承接者:无) · noted, not filed — the class stays open one level up. validationFailure(message, fields) in packages/types/src/validation-failure.ts types fields as unknown[], which is how both rows got past tsc. Typing the rows' code as FieldErrorCode would close the class at compile time for every caller in every package. That is a signature change to a published @objectstack/types export, outside this claim's file surface. It is recorded in PR #24683 Acceptance notes.",
    "carrier: none (承接者:无) · noted, not filed — a read-only AST sweep of packages/*/src (tests excluded), for literal rows {field, code, message} whose string-literal code is not a FieldErrorCode member, found three hits in packages/metadata-protocol/src/protocol.ts:5772/5799/5810 (field_removed, field_type_change, field_required_no_default). Those are DESTRUCTIVE_CHANGE findings delivered as err.issues, not details.fields[], so they are not this catalog's position. The sweep sees only string-literal codes, so it is a supporting reading, not a proof. Recorded in PR #24683 Acceptance notes."
    ],
    "deviations": "Two items in the local run go beyond os-dev.md's floor, both because the dispatch asked: test:repo, and pnpm lint as the full union. No origin/main merge was made, per the dispatch. A scratch envelope-printer test file was created in the worktree for the before/after measurement and deleted before the commit; it was never staged."
    }

  5. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24683 comment 6279556446 · domain:cli seat 1 · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-19T23:24Z · head 5fd5f5e71. Landing waits on that head's checks.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24683 → 3b8e2b145 · domain:cli seat 1 · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-20T00:10Z

    Reading Result
    PR #24683 object merged: true, merge commit 3b8e2b145, merged 2026-10-20T00:09Z through the merge queue
    origin/main 3b8e2b145 is an ancestor of origin/main, with a single parent (squash). The 4 files and +363 / −2 match what was reviewed at 5fd5f5e71
    queue branch no gh-readonly-queue/main/*pr-24683* head remains
    • Review chain: claim 6278722621 → report 6279499710 → ACCEPT 6279556446 → ready and auto-merge.
    • What landed: two hand-built 400 rows now carry ADR-0114 FieldErrorCode catalog members. The run-lifecycle body refusal (cancel, restore-suspension) carries unknown_field, not unrecognized_keys. The permission-set clone name rule carries invalid_format, not invalid_string. field-error-code-catalog.test.ts pins both sites and runs every hand-built row under packages/runtime/src/domains/ through the catalog (41 tests).
    • Acceptance notes carried forward: validationFailure(message, fields) in packages/types types fields as unknown[]. A typed row would close the class at compile time. This is recorded for the next seat; not filed.
    • State: the card closed completed on the merge. pm:dispatched is removed in the stroke after this comment.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions