Skip to content

ItxError: five-code structured kernel errors over capnweb#1456

Merged
jonastemplestein merged 2 commits into
mainfrom
itx-error-codes
Jun 10, 2026
Merged

ItxError: five-code structured kernel errors over capnweb#1456
jonastemplestein merged 2 commits into
mainfrom
itx-error-codes

Conversation

@jonastemplestein

@jonastemplestein jonastemplestein commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

What

Replaces the message-shape regex error detection in the itx react client with structured ItxErrors thrown by the kernel and read back by code, end to end across capnweb. Immediate cutover: server and client deploy together; the regex is gone.

Design

  • apps/os/src/itx/errors.ts (new, transport-agnostic): ItxError extends Error with own enumerable code and optional details — exactly the properties capnweb (0.8.0) serializes (["error", name, message, stack?, props?]). The receiver reconstructs a plain Error, so detection is duck-typed via getItxErrorCode(error) — never instanceof. No rehydration layer.
  • Exactly five codes: NOT_FOUND, FORBIDDEN, CONFLICT, BAD_REQUEST, INTERNAL. No UNAUTHORIZED — itx auth happens at connect (Law 3), so auth failures are transport-level 401s before a session exists.
  • Existence masking preserved: itx.projects.get / context resolution answer byte-identical NOT_FOUND for missing AND forbidden, so error shapes can't be used to probe which project ids/slugs exist. FORBIDDEN only where existence is established or not secret (global streams, append policy, create/remove projects).
  • Tag, don't redact: the /api/itx sessions get onSendError: tagOutboundItxError — every outbound non-ItxError becomes ItxError { code: "INTERNAL" } with the original message and stack preserved; returning the error from the hook is also what opts the stack into transmission (we trust our callers). capnweb's newWorkersRpcResponse wrapper takes no options, so fetch.ts re-implements its two-line dispatch as newItxRpcResponse.
  • details ships in v1: throw sites attach what they have ({ projectIdOrSlug }, { path, policyMode }, conflict slugs/ids).
  • /api/itx/run: HTTP error bodies now carry code (400/403/404/500/503); the script isolate threads a thrown ItxError's code through its JSON outcome.
  • Client: useItxQuery retries only code-less (socket) or INTERNAL errors, at most once; the stream-tail multiplexer keeps skipping retry exactly for access errors (NOT_FOUND/FORBIDDEN).

Tests

  • react/errors.test.ts rewritten: code-based detection, own-enumerability of code/details (the load-bearing wire property), simulated capnweb crossing (instanceof lost, code/details survive), tagOutboundItxError.
  • stream-tail.test.ts: access-error case now throws the post-capnweb shape (plain Error + name/code).
  • Worker harness (pnpm test:itx-stream-subscribe): new case proves code/details survive real Workers RPC hops (StreamsCapability → loopback → harness → test) — 9/9 pass, so kernel throws born behind the loopback keep their codes on the way to capnweb.
  • e2e (itx.e2e.test.ts): itx.projects.get("definitely-not-a-project") rejects with name ItxError, code NOT_FOUND, details — runs against a deployment in preview CI.

Docs

  • Doc comments on ItxError (wire mechanics, taxonomy, masking, posture) and the onSendError wiring.
  • DECISIONS.md D18; oRPC replacement plan: kernel-hardening item recorded as done, "Error opacity" risk removed; task checkbox ticked.

Verification

pnpm typecheck && pnpm lint && pnpm format && pnpm test all green at repo root; pnpm test:itx-stream-subscribe 9/9.

🤖 Generated with Claude Code


Note

Medium Risk
Touches auth-sensitive error shapes (existence masking), all /api/itx RPC sessions, and client retry behavior; coordinated deploy required but well-tested across RPC boundaries.

Overview
Introduces ItxError with five codes (NOT_FOUND, FORBIDDEN, CONFLICT, BAD_REQUEST, INTERNAL) as own enumerable props so capnweb can serialize them; clients detect errors via getItxErrorCode / isItxAccessError, not instanceof or message regexes.

Kernel throw sites in handle.ts, streams-capability.ts, and related paths now raise ItxError with masking preserved (NOT_FOUND for missing and forbidden projects). fetch.ts wires onSendError: tagOutboundItxError through a custom newItxRpcResponse (non-ItxErrors become INTERNAL with stack); HTTP /api/itx/run and connect failures return JSON code fields. Script runner threads code through isolate JSON outcomes.

React layer drops regex-based access-error detection; useItxQuery retries only code-less or INTERNAL errors; stream-tail tests use post-capnweb error shapes. Docs (D18, plan, teardown task) mark kernel error hardening done. New e2e and worker-harness tests assert code/details survive capnweb and Workers RPC.

Reviewed by Cursor Bugbot for commit 7661b13. Bugbot is set up for automated code reviews on this repo. Configure here.

Environment Config Lease

No active environment config lease.

OS

Status: released
Commit: 7661b13
Preview: https://os.iterate-preview-3.com
Summary: Preview app released.
Workflow run
Updated: 2026-06-10T14:45:38.214Z

jonastemplestein and others added 2 commits June 10, 2026 15:29
New kernel module apps/os/src/itx/errors.ts: ItxError (Error + own
enumerable code/details, duck-typed detection via getItxErrorCode — class
identity does not survive capnweb), exactly five codes (NOT_FOUND,
FORBIDDEN, CONFLICT, BAD_REQUEST, INTERNAL; no UNAUTHORIZED — auth is a
transport 401 at connect), existence masking preserved (missing and
forbidden projects both answer byte-identical NOT_FOUND), and
tag-don't-redact: the /api/itx sessions' onSendError rewrites every
non-ItxError to INTERNAL keeping message + stack, which also opts stacks
into transmission.

- handle.ts / streams-capability.ts kernel throws converted to coded
  ItxErrors with details ({ projectIdOrSlug }, { path, policyMode }, ...)
- fetch.ts: newItxRpcResponse dispatcher (capnweb's wrapper takes no
  RpcSessionOptions); /api/itx and /api/itx/run HTTP error bodies carry
  the code; run isolate threads a thrown ItxError's code into its JSON
- react client: message-shape regex deleted; getItxErrorCode /
  isItxAccessError re-exported from the kernel module; useItxQuery
  retries only code-less or INTERNAL errors (once); stream-tail keeps
  skipping retry exactly for access errors
- tests: code-based unit tests incl. simulated capnweb crossing and
  own-enumerability; worker harness proves code/details survive real
  Workers RPC hops; e2e asserts projects.get of a missing project
  rejects ItxError/NOT_FOUND over live capnweb
- docs: DECISIONS.md D18; oRPC replacement plan updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jonastemplestein jonastemplestein merged commit f08a722 into main Jun 10, 2026
8 of 9 checks passed
@jonastemplestein jonastemplestein deleted the itx-error-codes branch June 10, 2026 14:43
jonastemplestein added a commit that referenced this pull request Jun 10, 2026
Conflict resolution and follow-ups:
- itx-stream-subscribe harness/test: keep main's ItxError
  appendOutsidePolicy surface (#1456) alongside this branch's
  getState-based child-path tests (list() stays removed)
- codemode-mcp-provider-stack e2e (Bugbot): ctx.os only forwards unary
  project.* oRPC procedures, so ctx.os.streams.get("/").getState() could
  never resolve — use project.streams.read({ streamPath: "/" }) instead
  (this test is skipped in CI, so the green check never exercised it)
- itx REPL ambient typings: drop streams.list() — the procedure no
  longer exists on this branch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jonastemplestein added a commit that referenced this pull request Jun 10, 2026
capnweb's receiver rebuilds a plain Error — custom names and class
identity never survive the wire, which is exactly why D18 makes
detection duck-typed. The e2e contradicted its own doc comment by
asserting error.name === "ItxError"; it now asserts getItxErrorCode +
details, the actual contract. (This assertion also failed inside PR
#1456's Preview e2e job, which nevertheless reported success — the
preview runner swallowing a suite failure is a separate problem,
recorded in the PR thread.)

Also gives the two-event /api/itx/run record test 90s: the cold first
run of isolate + stream DO on a fresh preview blew the 45s default and
passed on the in-job retry at 12s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jonastemplestein added a commit that referenced this pull request Jun 10, 2026
…txError test fix

- Single-flight ensureItxContext in the Agent DO and MCP connection
  (Bugbot High: wake-time workspace prep vs script runs could mint two
  context ids; concurrent exec_js likewise).
- MCP session seeding gains MCP_CONTEXT_CAPS_VERSION (same re-seed-on-
  version-bump semantics the agent already had).
- runItxScript gains convention: "ctx" — agent/LLM scripts (async (ctx)
  => …) are invoked directly, killing the async ({ itx, vars }) wrapper
  so the execution record carries exactly what the model wrote. vars is
  an "itx"-convention concern only.
- ItxError e2e test corrected: capnweb 0.8.0 reconstructs unknown error
  names as plain Error and DROPS the name (ERROR_TYPES[name] || Error;
  props loop skips "name"), so name identity is untransmittable — the
  test (which merged in #1456 with its e2e check skipped, so it never
  ran) now asserts the duck-typed code/details contract; errors.ts doc
  corrected to match reality.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jonastemplestein added a commit that referenced this pull request Jun 10, 2026
The preview e2e caught a real regression shipped in #1456: capnweb's
receiver rebuilds `new Error(message)` and never assigns the custom
name (verified in the 0.8.0 deserializer and against a live preview),
so the `name === "ItxError"` gate rejected every wire-crossed kernel
error — getItxErrorCode returned undefined, access errors were retried
again, and the access-denied UI never fired. The unit suite missed it
because its simulated crossing unfaithfully copied the name.

Detection is now code-only (the five-code set; foreign code strings
like ENOENT are outside it), the simulation matches the real receiver
(name comes back "Error" and a test pins that), and the doc comments
record why the name must not participate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant