From 343705cc3427b408cc970488ef116ee2e1b07de5 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 4 Oct 2026 15:37:09 -0700 Subject: [PATCH 1/4] Add tests for id leak guard covering every generateId prefix --- apps/web/src/chat/wire/id-leak-guard.test.ts | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 apps/web/src/chat/wire/id-leak-guard.test.ts diff --git a/apps/web/src/chat/wire/id-leak-guard.test.ts b/apps/web/src/chat/wire/id-leak-guard.test.ts new file mode 100644 index 0000000000..f4872b230c --- /dev/null +++ b/apps/web/src/chat/wire/id-leak-guard.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "bun:test"; +import { generateId } from "../../../../../vendor/intx/hub-common/src/ids"; +import { assertNoLeakedInternalId } from "./id-leak-guard"; + +const KINDS: Parameters[0][] = [ + "tenant", + "principal", + "principalKey", + "role", + "grant", + "federationTrust", + "provider", + "oauthClient", + "credential", + "wallet", + "transaction", + "offering", + "model", + "modelProvider", + "modelOffering", + "modelPricing", + "session", + "sessionMail", + "inferenceTurn", + "turnPart", + "asset", + "gitToken", + "workflowRun", + "approval", + "signal", + "workflowDefinition", + "workflowDefinitionVersion", +]; + +describe("assertNoLeakedInternalId", () => { + test.each(KINDS)("catches a generated %s id", (kind) => { + const id = generateId(kind); + expect(() => assertNoLeakedInternalId(id, "title")).toThrow("internal identifier"); + expect(() => assertNoLeakedInternalId(`Open ${id} now`, "title")).toThrow( + "internal identifier", + ); + }); + + test("passes ordinary text", () => { + expect(() => assertNoLeakedInternalId("Weekly digest", "title")).not.toThrow(); + }); +}); From ee96091e085fe57c3b6a0463e23eeef2e9959eae Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 4 Oct 2026 15:37:20 -0700 Subject: [PATCH 2/4] chat wire: id leak guard mirrors every generateId prefix (CL-9910) --- apps/web/src/chat/wire/id-leak-guard.ts | 34 ++++++++++++++++++++++--- docs/chat-wire-contract.md | 11 ++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/apps/web/src/chat/wire/id-leak-guard.ts b/apps/web/src/chat/wire/id-leak-guard.ts index a211c119d0..37ec95bab8 100644 --- a/apps/web/src/chat/wire/id-leak-guard.ts +++ b/apps/web/src/chat/wire/id-leak-guard.ts @@ -1,9 +1,37 @@ -// Mirrored from packages/chat/src (see docs/chat-wire-contract.md, which -// also covers this file's rationale). +// Mirrors PREFIXES in vendor/intx/hub-common/src/ids.ts (see +// docs/chat-wire-contract.md, which also covers this file's rationale). // Matches `_`, space, or `-` as separator so both the raw id and // `humanizeSlug`'s Title-Cased reading of one ("Run 737a058d…") trip it. -const ID_PREFIX_WORDS = ["run", "wfd", "tnt", "prn", "ast", "gtk"] as const; +const ID_PREFIX_WORDS = [ + "tnt", + "prn", + "pky", + "rol", + "grt", + "ftr", + "prv", + "ocl", + "crd", + "wlt", + "txn", + "ofr", + "mdl", + "mpv", + "mof", + "mpr", + "ses", + "sml", + "itn", + "tp", + "ast", + "gtk", + "run", + "apr", + "sig", + "wfd", + "wdv", +] as const; export const ID_LEAK_PATTERN = new RegExp( `\\b(?:${ID_PREFIX_WORDS.join("|")})[_\\s-][0-9a-f]{16,}\\b`, diff --git a/docs/chat-wire-contract.md b/docs/chat-wire-contract.md index bde4ac3374..09272707f2 100644 --- a/docs/chat-wire-contract.md +++ b/docs/chat-wire-contract.md @@ -1,10 +1,9 @@ # Chat wire contract `apps/web/src/chat/wire/*` is the browser-facing half of the wire contract -also implemented server-side in `packages/chat/src`. `apps/web` cannot import -`@corbits/chat` — a server-only package — so this half is hand-mirrored -rather than shared by import. Once the hub moves onto native mail threads, -`packages/chat`'s copy goes away and this becomes the one source of truth. +a hand-mirrored copy of the server-side contract: `apps/web` cannot import +server-only `@intx/*` packages, so this half is mirrored rather than shared +by import. Keep both sides in sync by hand until then; a schema or type added to one side belongs on the other. @@ -17,7 +16,7 @@ Title-Cased reading of one, e.g. "Run 737a058d…"). This recurred repeatedly as one-off display-time patches (insights, a worker reply, a chat title) before being consolidated here, so a new id-generating prefix is a missed test run rather than a missed grep. Prefix words mirror `@intx/hub-common`'s -`generateId` (`PREFIXES` in `packages/hub-common/src/ids.ts`). +`generateId` (`PREFIXES` in `vendor/intx/hub-common/src/ids.ts`). ## Approve block (`chat/blocks/approve-block.tsx`) @@ -50,7 +49,7 @@ already gave the connecting stream. By the time a reply reaches the chat timeline it is a plain text part with no metadata — the failure's category is structured further upstream -(`packages/chat`'s orchestrator reads it directly), but nothing carries it +(the server-side orchestrator reads it directly), but nothing carries it down to this render layer. So this file matches reply prose against the exact preambles `@intx/inference`'s `formatInferenceError` writes for `credential_failure` and `quota_exhausted`, anchored at the start of the From f14e7a492c66330ce3af3823ec4ea3e468776c64 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 4 Oct 2026 15:39:55 -0700 Subject: [PATCH 3/4] Update docs: wire headers drop the nonexistent packages/chat reference (CL-9910) --- apps/web/src/chat/wire/agent-address.ts | 2 +- apps/web/src/chat/wire/blocks.ts | 4 ++-- apps/web/src/chat/wire/consumer-inference-text.ts | 2 +- apps/web/src/chat/wire/display-name.ts | 2 +- apps/web/src/chat/wire/mentions.ts | 2 +- apps/web/src/chat/wire/participants.ts | 2 +- apps/web/src/chat/wire/parts.ts | 2 +- apps/web/src/chat/wire/stream-events.ts | 2 +- apps/web/src/chat/wire/tools-unsupported.ts | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/web/src/chat/wire/agent-address.ts b/apps/web/src/chat/wire/agent-address.ts index 0e655ef7f5..c5534cca33 100644 --- a/apps/web/src/chat/wire/agent-address.ts +++ b/apps/web/src/chat/wire/agent-address.ts @@ -1,4 +1,4 @@ -// Mirrored from packages/chat/src (see docs/chat-wire-contract.md). +// Browser-side wire contract (see docs/chat-wire-contract.md). // Deliberately permissive, unlike `@intx/types`'s `parseAgentAddress`: the // mention rule only needs the substring before "@", not a validated id. diff --git a/apps/web/src/chat/wire/blocks.ts b/apps/web/src/chat/wire/blocks.ts index 92e4923d5c..40d0c6e9a8 100644 --- a/apps/web/src/chat/wire/blocks.ts +++ b/apps/web/src/chat/wire/blocks.ts @@ -1,5 +1,5 @@ -// Mirrored from packages/chat/src (see docs/chat-wire-contract.md) — apps/web -// must not import @corbits/chat, a server-only package. +// Browser-side wire contract (see docs/chat-wire-contract.md) — apps/web +// must not import server-only packages. import { type } from "arktype"; diff --git a/apps/web/src/chat/wire/consumer-inference-text.ts b/apps/web/src/chat/wire/consumer-inference-text.ts index 7a6c57ef10..08e69f8388 100644 --- a/apps/web/src/chat/wire/consumer-inference-text.ts +++ b/apps/web/src/chat/wire/consumer-inference-text.ts @@ -1,4 +1,4 @@ -// Mirrored from packages/chat/src (see docs/chat-wire-contract.md). +// Browser-side wire contract (see docs/chat-wire-contract.md). // HTTP status, raw provider dumps, and JSON error objects never belong on // the timeline — DESIGN.md Honesty is one consumer sentence. diff --git a/apps/web/src/chat/wire/display-name.ts b/apps/web/src/chat/wire/display-name.ts index 426964d7b7..8715e18416 100644 --- a/apps/web/src/chat/wire/display-name.ts +++ b/apps/web/src/chat/wire/display-name.ts @@ -1,4 +1,4 @@ -// Mirrored from packages/chat/src (see docs/chat-wire-contract.md). +// Browser-side wire contract (see docs/chat-wire-contract.md). // One derivation for "what does this agent look like to a person", never a // scattered `description ?? name` reimplemented per call site. diff --git a/apps/web/src/chat/wire/mentions.ts b/apps/web/src/chat/wire/mentions.ts index ff36c43c1a..c0178c2659 100644 --- a/apps/web/src/chat/wire/mentions.ts +++ b/apps/web/src/chat/wire/mentions.ts @@ -1,4 +1,4 @@ -// Mirrored from packages/chat/src (see docs/chat-wire-contract.md). +// Browser-side wire contract (see docs/chat-wire-contract.md). // The composer and the server fan-out both derive mentions from the same // `ParticipantRecord[]`, so what the composer highlights is always exactly diff --git a/apps/web/src/chat/wire/participants.ts b/apps/web/src/chat/wire/participants.ts index 7c2e85a0ad..7c8608c79e 100644 --- a/apps/web/src/chat/wire/participants.ts +++ b/apps/web/src/chat/wire/participants.ts @@ -1,4 +1,4 @@ -// Mirrored from packages/chat/src (see docs/chat-wire-contract.md). +// Browser-side wire contract (see docs/chat-wire-contract.md). // Reading tolerates a bare address string (pre-rollout data) and upgrades it // to a record; writing always produces records, never bare strings. diff --git a/apps/web/src/chat/wire/parts.ts b/apps/web/src/chat/wire/parts.ts index 47efb9008f..8e57c710eb 100644 --- a/apps/web/src/chat/wire/parts.ts +++ b/apps/web/src/chat/wire/parts.ts @@ -1,4 +1,4 @@ -// Mirrored from packages/chat/src (see docs/chat-wire-contract.md). +// Browser-side wire contract (see docs/chat-wire-contract.md). import { type } from "arktype"; diff --git a/apps/web/src/chat/wire/stream-events.ts b/apps/web/src/chat/wire/stream-events.ts index 90ca8eb54f..594c85c6ba 100644 --- a/apps/web/src/chat/wire/stream-events.ts +++ b/apps/web/src/chat/wire/stream-events.ts @@ -1,4 +1,4 @@ -// Mirrored from packages/chat/src (see docs/chat-wire-contract.md, which +// Browser-side wire contract (see docs/chat-wire-contract.md, which // also covers this file's organizing rule). import { type } from "arktype"; import { Part } from "./parts"; diff --git a/apps/web/src/chat/wire/tools-unsupported.ts b/apps/web/src/chat/wire/tools-unsupported.ts index 586c3b2b57..fbea8f2380 100644 --- a/apps/web/src/chat/wire/tools-unsupported.ts +++ b/apps/web/src/chat/wire/tools-unsupported.ts @@ -1,4 +1,4 @@ -// Mirrored from packages/chat/src (see docs/chat-wire-contract.md). +// Browser-side wire contract (see docs/chat-wire-contract.md). export const TOOLS_UNSUPPORTED_CONSUMER_MESSAGE = "This agent's model can't use tools."; From 10ef85863b122bf651443fd7f8acdfa750e0cbbb Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 4 Oct 2026 15:56:43 -0700 Subject: [PATCH 4/4] Update docs: rewrite chat wire contract intro (CL-9910) --- docs/chat-wire-contract.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/chat-wire-contract.md b/docs/chat-wire-contract.md index 09272707f2..fe537239c6 100644 --- a/docs/chat-wire-contract.md +++ b/docs/chat-wire-contract.md @@ -1,12 +1,10 @@ # Chat wire contract -`apps/web/src/chat/wire/*` is the browser-facing half of the wire contract -a hand-mirrored copy of the server-side contract: `apps/web` cannot import -server-only `@intx/*` packages, so this half is mirrored rather than shared -by import. - -Keep both sides in sync by hand until then; a schema or type added to one -side belongs on the other. +`apps/web/src/chat/wire/*` holds the browser-side wire shapes and guards for +chat. `apps/web` cannot import server-only `@intx/*` packages, so anything +it needs from them (for example the `@intx/hub-common` id prefixes) is +mirrored here by hand. There is no server-side copy in this repo to keep in +sync with. ## Internal-id leak guard