Skip to content

fix(flair-client): errors name only the entry point, method, path, status and a short server code - #598

Merged
tps-flint merged 14 commits into
mainfrom
fix/597-flair-client-error-redaction
Oct 12, 2026
Merged

tps-flint merged 14 commits into
mainfrom
fix/597-flair-client-error-redaction

Conversation

@tps-anvil

@tps-anvil tps-anvil commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #597.

A non-OK Flair response error contains the entry point, method, requested pathname, HTTP status and an allow-listed code (else unrecognized_error). The pathname comes from the URL actually fetched, excluding query and fragment. Encoded parameters stay encoded when the wrapper or caller encodes them; request() sends its caller's path. The client reads an allow-listed string from the response's error field when present. These response error messages exclude raw bodies, other response text and transport messages.

The client previously included raw response bodies and propagated transport messages and causes. The replacement formats are:

  • Non-OK response errors emitted by request, registerAgent and readStoredPublicKey use Flair <entry> <method> <pathname> → <status>: <code>. An error value outside the fixed list becomes unrecognized_error, including an unlisted code-shaped credential fragment. readStoredPublicKey returns an absent result on 404 instead of throwing. Wrapper methods encode ids with encodeURIComponent; the changed direct callers encode flair-sync's memory id, flair-task-loop's participant id and flair-health's cleanup id. For these encoded paths, an id containing #, ?, / or % remains a single encoded segment; readStoredPublicKey reports /Agent/agent%2Fsub for agent/sub.
  • Caught transport failures use Flair <entry> <method> <pathname> transport error: <reason> with only a fixed reason class and no message text, including when inspecting the failure throws. An object-valued failure's or cause's code selects the reason class: connection refused, timeout, connection reset, dns failure, TLS failure, or the fallback network failure; a callable's own code is ignored (an object-valued cause on it can still select a reason); any other candidate value or a throw during inspection uses the fixed fallback.
  • A failed successful-response JSON parse uses Flair <entry> <method> <pathname> → <status>: malformed response body. request() and registerAgent() accept 204 without parsing; readStoredPublicKey() attempts parsing on successful responses.
  • Local checks use formats with variable fields, outside the response bound: promotion refusals interpolate the action and memory id; private-key read failures interpolate the key path and agent id; invalid principal records and missing-agent checks interpolate agent ids; governance read refusals interpolate memory ids and, for caught read failures, the caught error's message. Underlying URL, key parsing, signing and serialization errors are outside the response-error guarantee.

redactCredentialMaterial and its helpers are removed.

The allow-list

A shape test can accept a code-shaped credential fragment. The fixed list restricts the response error's code to an enumerated string or unrecognized_error. A listed string can coincidentally match credential material; list membership does not establish route reachability.

FLAIR_ERROR_CODES is exported as a frozen array for inspection; membership uses a module-private Set. Attempts to mutate the exported view cannot change response formatting. A ReadonlySet alone is a type-only guarantee; a live exported Set is mutable at runtime. The list is not claimed complete, and no entry is claimed reachable from a particular route. There are no per-route or per-emitter annotations in the code, this body or the changelog.

Test

packages/cli/test/flair-client-redaction.test.ts drives each entry point in a Bun child importing the source src/utils/flair-client.ts and a Node child importing the built dist/src/utils/flair-client.js. The server echoes the full Authorization value in text, JSON and malformed-JSON successful bodies; the tests assert its presence in the fixture body before checking its absence from the error. The JSON error field carries a code-shaped credential fragment. The signed path uses the verifying stub (helpers/stub-flair.ts) with a registered signer and tests refusals for an unregistered signer and a bad signature. The Basic paths use an authorized operator fixture and test refusal of a different credential.

The echo and transport cases assert the whole message by exact equality: Flair <entry> <method> <pathname> → <status>: unrecognized_error for non-OK text and JSON bodies, Flair <entry> <method> <pathname> → <status>: malformed response body for the malformed successful body, and Flair <entry> <method> <pathname> transport error: connection refused for mocked transport failures. The native closed-port case asserts Flair request GET /Memory/mem-1 transport error: connection refused. Appending response text to the messages exercised by these assertions fails them. A response-body append mutation was reported in the earlier-round Evidence below. Echo and mocked-transport assertions also check fixture credentials, their eight-character windows, Authorization values and signature material against the captured message, stack-visible fields and cause chain. The JSON case checks the fallback and the fragment's absence. The printing-caller test invokes runAgent({ action: "create", ... }) directly and captures console output from its read-back remedy.

The coverage case sends each listed code over a mocked request GET /Memory/mem-1 response and asserts Flair request GET /Memory/mem-1 → 400: <code> by exact equality. Its off-list examples — a code-shaped credential fragment, unlisted strings and non-strings — yield unrecognized_error. It tests the fixed-list property without asserting route, verb or emitter reachability. The mutation case attempts push, add, an element write, a truncate and a delete on the exported view; an injected response value still yields unrecognized_error. If the live membership Set were exported, the add attempt would insert the injected code. The path case drives ids containing #, ?, / and % through getMemory (which calls request) and registerAgent, checking encoded pathnames against those recorded by the echo server. The suites asserting the previous message format are updated (agent-healthcheck, mail-flair-read-auth, cli-flag-dispatch, mail-verify-key-encoding, memory-entry).

Evidence

Previously reported measurements on head 3a6cdafe (main 36c8a156), each lane HOME-isolated (HOME=<throwaway> bun run test) with an empty launcher HOME.

  • Mutations red, then green.
    • Exporting the live membership Set again (flair-client.ts) and rebuilding makes the redaction suite red — 11 pass, 2 fail: the coverage case (.length on a Set) and the mutation case, whose failing assertion is the exact message — the injected code is named verbatim (Flair request GET /Memory/mem-1 → 400: injected_not_a_flair_code where the constant was expected). Reverting and rebuilding is green — 13 pass, 0 fail.
    • Appending the first 16 characters of the response body to responseError's message (flair-client.ts) and rebuilding makes the redaction suite red — 7 pass, 6 fail (the text and JSON echo cases per runtime plus the coverage and mutation cases). Reverting and rebuilding is green — 13 pass, 0 fail.
  • Full lane (bun run test, HOME-isolated) — head 3a6cdafe: agent 194 pass/0 fail (18 files); cli 3087 pass/6 skip/0 fail (3093 tests, 201 files); root-test 283 pass/2 todo/0 fail (285 tests, 13 files). origin/main 36c8a156: agent 194 pass/0 fail (18 files); cli 3074 pass/6 skip/0 fail (3080 tests, 200 files); root-test 283 pass/2 todo/0 fail (285 tests, 13 files).
  • bun run lint:ci clean; node scripts/changelog-fragments.mjs check clean (80 fragments); bun run build clean on head 3a6cdafe (and on origin/main for the comparison run).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security and Reliability
    • Flair request errors now show the request path, status, and a recognized error code without exposing raw server response text or credentials.
    • Network failures and malformed responses use standardized, sanitized messages.
    • IDs in Flair request paths are encoded to handle special characters safely.

Errors built from a response body or a transport failure carried the request's
credential verbatim. Redact the exact credential values the request sent — the
Authorization header value, and the operator credential a Basic header was
built from — wherever the client builds an error from a response body or a
transport failure.
@tps-anvil
tps-anvil requested a review from a team as a code owner October 11, 2026 10:09
@tps-anvil

Copy link
Copy Markdown
Collaborator Author

cli#597 — added/changed prose sweep

Every sentence this PR adds or changes (changelog fragment, source comments and
strings, test comments and titles, PR body), checked against the code for
(a) outcomes/status codes on every branch, (b) scope words, (c) coverage claims.
Verdict per sentence: PASS (true on every branch) or FIXED (rewritten).

Measured on 054fa2d (main 36c8a15).

Changelog fragment (.changelog/unreleased/fixed-597-flair-client-error-redaction.md)

  1. "- The Flair client redacts the request's credential from error messages
    built from a response body or a transport failure (Closes Flair client errors never carry credential material #597).
    " —
    PASS. Scope named: a response body or a transport failure. The three methods
    that do either (request, registerAgent, readStoredPublicKey) all route
    through the helper; no status code is claimed.
  2. "A server that echoes the request's credential back in a response body no
    longer puts it into a thrown error message;" — PASS. origin/main's
    request/registerAgent/readStoredPublicKey included res.text()
    verbatim; the diff replaces that with redactCredentialMaterial(text, …).
  3. "a transport failure's message is redacted the same way." — PASS. Each
    fetch call is wrapped and its thrown message routed through the helper.

Source comments and strings (packages/cli/src/utils/flair-client.ts)

  1. "/** What a redacted credential is replaced with. */" — PASS.
  2. redactCredentialMaterial doc: "Redact credential material from text an error
    is built from. secrets are the exact credential values the request carried
    — an Authorization header value, or the operator credential a Basic header
    was built from. Each occurrence of a value is replaced with [REDACTED]." —
    PASS. Matches the split/join implementation; no coverage claim.
  3. "/** The message of an unknown thrown value. */" — PASS.
  4. "/** The credential values a Basic-authenticated request carries. */" —
    PASS. basicAuthSecrets returns the header, its base64 payload and the raw
    operator credential.
  5. Error strings "transport error:" (request, readStoredPublicKey, registerAgent)
    and "…failed: : …" (registerAgent) — PASS. The status is the real
    res.status on the non-OK branch; the transport branch carries no status.

Test (packages/cli/test/flair-client-redaction.test.ts)

  1. "cli#597 — redact credential material from the CLI's Flair client errors." —
    PASS.
  2. "The entry points that surface errors (request, registerAgent,
    readStoredPublicKey) are driven through both, and the test asserts the
    credential is absent from the thrown error's message and cause chain and
    from the console output it captures." — FIXED. Was "Each client entry
    point…"; rewritten to name the three entry points the test drives, since
    "each…that surfaces errors" was broader than the test's cases.
  3. "/** Echoes the caller's credential into a body, a response header, and (for
    a Basic credential) its decoded value. */" — PASS. The handler sets
    X-Echo-Authorization and writes the value into body text or a JSON field;
    for a Basic header it also appends the decoded credential.
  4. "/** Replace fetch with one that fails, carrying the request's credential in
    the message and the cause. */" — PASS.
  5. "/** The messages of an error and its cause chain (up to eight links). */"
    — FIXED. Was "Every message reachable from an error"; rewritten to state the
    eight-link bound.
  6. Titles "cli#597: the Flair client redacts the request's credential from
    error output", "…redacts a credential echoed in an error body", "…in a JSON
    body", "…from a transport failure" — PASS. Each names the scenario it
    exercises; no scope word beyond it.

PR body

  1. Claimed counts and citations. The head SHA, the two lane counts and the
    mutation counts are pasted from this run's logs, each naming the commit it
    was measured on. PASS.

Sweep run:
git diff -U0 origin/main...HEAD -- '*.md' .changelog docs '*.ts' | grep '^+' | grep -inwE 'only|never|every|always|exactly|guarantee|cannot|all|none|any' and
the same over the PR body and the added test titles — no hits remain.

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d4b9a75a-78c0-4ed0-a239-438331c32954

📥 Commits

Reviewing files that changed from the base of the PR and between 36c8a15 and 22b4161.


📒 Files selected for processing (12)
  • .changelog/unreleased/fixed-597-flair-client-error-redaction.md
  • packages/cli/src/commands/flair-health.ts
  • packages/cli/src/commands/flair-sync.ts
  • packages/cli/src/utils/flair-client.ts
  • packages/cli/src/utils/flair-task-loop.ts
  • packages/cli/test/agent-healthcheck.test.ts
  • packages/cli/test/cli-flag-dispatch.test.ts
  • packages/cli/test/flair-client-redaction.test.ts
  • packages/cli/test/flair-client-transport-inspection.test.ts
  • packages/cli/test/mail-flair-read-auth.test.ts
  • packages/cli/test/mail-verify-key-encoding.test.ts
  • packages/cli/test/memory-entry.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.



📝 Walkthrough

Walkthrough

Flair client errors now use sanitized response, transport, and parse messages. Flair request paths encode IDs before sending requests. Tests cover error redaction, transport failures, and encoded paths.

Changes

Flair Client Changes

Layer / File(s) Summary
Flair error formatting and validation
.changelog/unreleased/fixed-597-flair-client-error-redaction.md, packages/cli/src/utils/flair-client.ts, packages/cli/test/flair-client-redaction.test.ts, packages/cli/test/flair-client-transport-inspection.test.ts, packages/cli/test/agent-healthcheck.test.ts, packages/cli/test/cli-flag-dispatch.test.ts, packages/cli/test/mail-flair-read-auth.test.ts, packages/cli/test/mail-verify-key-encoding.test.ts, packages/cli/test/memory-entry.test.ts
Flair response errors report the method, pathname, status, and an allow-listed error code without including raw response text. Transport and parse failures use fixed messages. Tests check credential leakage, error formatting, transport-failure classification, and affected command output.
Encoded IDs in Flair request paths
packages/cli/src/utils/flair-client.ts, packages/cli/src/commands/flair-health.ts, packages/cli/src/commands/flair-sync.ts, packages/cli/src/utils/flair-task-loop.ts, packages/cli/test/flair-client-redaction.test.ts
Flair client, command, and task-loop requests encode IDs in path segments. Tests check encoded IDs and the requested pathname.

Priority: ⬆️ High

Estimated code review effort: 3 (Moderate) | ~30 minutes

Change: Bug fix · Severity of issue fixed: High

Sequence Diagram(s)

sequenceDiagram
  participant CLICommand
  participant FlairClient
  participant FlairServer
  CLICommand->>FlairClient: Make Flair request
  FlairClient->>FlairServer: Send request
  FlairServer-->>FlairClient: Return response
  FlairClient-->>CLICommand: Return result or sanitized error
Loading

Suggested reviewers: heskew, tps-sherlock


Merge Risk: ⚪ Minimal · up to 22b41

No actionable issue remains from this review; the PR is mergeable after normal checks.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 11 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and specifically summarizes the main change: bounded Flair client errors that report only the entry point, method, path, status, and short server code.
Linked Issues check Passed Issue [#597] requires that Flair client errors do not expose credential material in response or transport failures. flair-client.ts constructs bounded non-OK response messages from the entry point, …
Out of Scope Changes check Passed The changes stay within issue [#597]. The allow-list, fixed transport classification, parse-error handling, and removal of response-body text implement bounded Flair client errors. ID encoding keeps r…

Full details: Docstring Coverage

Explanation

Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 11 files. (1 skipped: 1 unsupported.)


  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR


🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…atus and a short server code

Flair client errors contain only the entry point, method and path, status, and a
short server error code when present; response bodies and transport messages are
never included.

Exact-value redaction cannot be the control: a server that echoes the request's
credential back as a fragment, an encoding or a signature component survives it,
and a JSON parse error can quote a body fragment too. Build every error instead
from a whitelist of safe, client-constructed context:

- `request`, `registerAgent` and `readStoredPublicKey` name the entry point, the
  method and path (no query) and the HTTP status — plus the response body's short
  `error` code only when it matches /^[a-z0-9_.-]{1,64}$/.
- a transport failure is named by a fixed reason class derived from the error's
  code (Node carries it on `cause`), never its message.
- a successful response whose body is not JSON throws a safe error (caught at all
  three parse sites).

No response body, transport message, parser message or original `cause` is ever
included. `redactCredentialMaterial` and its helpers are removed — nothing needs
them once untrusted text is never included.

Tests drive each entry point, in-process and in a spawned `node` child against
the built dist, through a server that echoes the credential (whole, fragment,
base64 slice, signature component) in a text, JSON and malformed-JSON body and
through a transport failure whose message and cause quote the request. The echo
server sits behind the shared verifying stub for a registered signer, with its
invalid-signer refusal asserted first, and behind an authorized Basic fixture
with its own refusal. `tps agent create`'s read-back remedy is driven end to end
and its stdout/stderr asserted. Names are neutral.

Closes #597.
@tps-anvil tps-anvil changed the title fix(flair-client): redact credential material from Flair client errors fix(flair-client): errors name only the entry point, method, path, status and a short server code Oct 11, 2026
tps-flint and others added 10 commits October 11, 2026 15:14
…cli#597)

A regex shape test is not a control: a server `error` value can be
regex-shaped and still carry a credential fragment (a base64 Basic
slice, a nonce, a signature component). Replace `SAFE_ERROR_CODE` with
`FLAIR_ERROR_CODES`, a closed allow-list enumerated from Flair's
resources; any other value becomes the constant `unrecognized_error`.

Report the encoded request path the request was actually sent with:
`readStoredPublicKey` names `/Agent/agent%2Fsub`, as on the wire, not
`/Agent/agent/sub`.

Update the prose that overstated what an error contains, and the test
that drove the old shape.
… return (cli#597)

Drop from the allow-list every code a route this client sends cannot receive,
each checked against the Flair source: `slow_down` (rate-limit.ts, OAuth paths
only), `storage_quota_exceeded` (rate-limiter.ts, no caller), the four Soul
PATCH failure codes (Soul.ts patch() only; the client sends Soul PUT/DELETE),
`derived_from_self` (Memory.post only), `presence_query_failed` (Presence.get
only) and the OrgSkillAssignment codes (a resource the client never calls).

Encode the ids the direct request() callers build a path from (flair-sync,
flair-task-loop, flair-health); the wrappers already encode theirs, and
request() sends the path its caller supplies.

Tests: assert the WHOLE error message by exact equality for the echo cases and
the transport cases, so appending any response text fails; drive the coverage
case per route and verb rather than across resources.

Reword the prose to the bounded guarantee (entry point, method, requested
pathname, status and an allow-listed code; raw bodies, other response text and
transport messages never included) in the source comment, changelog and tests.
…le (cli#597)

The allow-list no longer claims per-route reachability: it is a fixed list of
error-code strings Flair defines, and any value not on it is
`unrecognized_error`. The guarantee depends only on the set being fixed and
closed, so the per-route and per-emitter annotations leave the code comment,
the coverage test, the changelog and the PR body.

The coverage test now asserts the property, not a route table: every listed
code is named verbatim, and any other value — including a code-shaped
credential fragment — becomes `unrecognized_error`. The fake route-semantics
server is removed; the exact-message echo/transport tests and their mutation
stay.

Text: "an error contains ..." applies to errors from NON-OK HTTP RESPONSES
only, and the other fixed formats (transport, malformed response, local) are
named once. A local error carries its own fixed message (not every local error
names its entry point).
…it covers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a thrown message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on value; object-only inspection stated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt says what is inspected

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tps-flint

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@tps-sherlock tps-sherlock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — the client now constructs error messages from fixed fields plus an allow-listed code, so no response body or transport text can enter them; the three error-building paths are all converted, the allow-list is module-private, and the redaction suite is mutation-sensitive. Two non-blocking notes (N1, N2).

Author is tps-anvil (a tps-* agent); repo tpsdev-ai/cli is public (checked via repos/tpsdev-ai/cli .visibility). Describing what the code guarantees, not how a credential could leak.

Kern's focus.

  • Every error built from server or transport text goes through the fixed format; nothing is missed. There are exactly four fetch sites in packages/cli/src/utils/flair-client.ts, and the three that can carry text are converted: request (:434), registerAgent (:485), readStoredPublicKey (:555) each wrap the fetch in try/catch → transportError(...), build a non-OK error via responseError(...), and wrap res.json() → parseError(...); the fourth (ping at :958) reads no body and returns res.ok. Non-Error throws are covered — transportError passes an unknown through transportReason, whose errorCode guards with its own try/catch and ignores callables. JSON parse failures are covered at all three sites, and request/registerAgent skip parsing on 204. The only other throw from a response (:525) interpolates an id, a local check.
  • Exact-value matching is replaced by an allow-list, which is the right shape. The non-OK error names only FLAIR_ERROR_CODE_SET.has(code) ? code : "unrecognized_error" — so an echoed credential (raw, Basic-base64, header form, or any encoded/truncated fragment) cannot survive, because the client names a constant it already holds rather than echoing server text. The pathname is new URL(url).pathname, so a credential in a query or fragment is never named; wrapper methods now encodeURIComponent their ids (getMemory, purgeMemory, proposeMemory, getAgent, updateAgent, the Soul methods) and the direct callers encode too, so the named path equals what the transport sent.
  • Neutral otherwise. The added helpers are pure string work on the error path; readStoredPublicKey now returns absent on 404 (previously threw). No change to a success path.

Sherlock's focus.

  • No partial or encoded fragment survives. A credential fragment (a base64 slice, a nonce, a signature component) that reaches the response's error field is not on the fixed list, so it becomes unrecognized_error; raw bodies and other response text are never read into the message; the transport message and the failure's cause are not re-attached (only a reason class from a fixed set). The suite pins this by exact-equality on the whole message and checks fixture credentials, 8-character windows, Authorization values and signature material against the captured message, stack-visible fields and the cause chain.
  • The allow-list is not mutable from outside. FLAIR_ERROR_CODES is a frozen array for inspection; membership uses a module-private Set, so the exported view cannot change formatting (the PR documents that a live exported Set/ReadonlySet would not be a guarantee).
  • The dropped cause loses little. A transport failure still names entry, method, pathname and a reason class (connection refused/timeout/connection reset/dns failure/TLS failure/network failure), which is what an operator triages on; the message text was the only part removed, and only that text was the leak vector.

N1 (non-blocking) — one local message embeds a caught error's text; safe today, worth a comment. readGovernedMemory (packages/cli/src/utils/flair-client.ts:721) does const detail = err instanceof Error ? err.message : String(err) and interpolates it. That is safe because the only thrower in the try is request(), whose FlairRequestError message is now sanitized; it would be worth one line saying so, so a future non-request throw added there is not assumed sanitized too.

N2 (non-blocking) — a few commands talk to Flair outside the client; I spot-checked them. roster.ts:258/276, init.ts:93, office-status.ts:138/149 and flair-health.ts:55 call fetch on Flair directly, so they sit outside this guarantee. I grepped packages/cli/src/commands for response-body reads into errors and found none (no .text()), so none echoes a body — but they are the residual surface to watch if any of them ever gains richer error reporting.

Evidence I measured (head 22b41611, ~/work/review-598-sherlock; built to dist for the Node child):

  • packages/cli/test/flair-client-redaction.test.ts + packages/cli/test/flair-client-transport-inspection.test.ts through the HOME-isolated launcher (TMPDIR=/tmp node scripts/test-suite.mjs cli …) → 43 pass / 0 fail, driving both the source (Bun child) and the built client (Node child).
  • Mutation, rebuilt then reverted: appending body.slice(0,16) to responseError's message → 7 pass / 6 fail (the text/JSON echo cases per runtime plus the coverage and mutation cases), matching the PR's claim; restored, tree clean.
  • Read against the helpers (responseError/transportError/parseError/responseErrorCode/errorCode/transportReason), the allow-list, and every fetch/throw site in the client plus the touched command files.
  • I did not run the full lane and did not evaluate CI (merge-gating is yours).

@tps-kern tps-kern left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — cli#598 (the CLI's Flair client errors never carry credential material; Closes #597) — Kern

APPROVE on head 22b41611. All three of my assigned focus items verify — by enumeration, by a main-control, and by a three-mutation matrix. Repo checked public (repos/tpsdev-ai/cli .visibility, re-verified today); per the brief, this review states what the code guarantees.

Design note, for the record. The head supersedes the brief's described mechanism (an exact-value redaction helper) with a strictly stronger structural bound: no response body, header or transport message text enters any error the client builds — non-OK errors name only the entry point, method, requested pathname, status, and one of the module's own allow-listed codes (else unrecognized_error); transport failures name only a fixed reason class; parse failures a fixed string. An enumeration-based redaction can miss an encoding; a bound that never admits the text cannot. I reviewed the head's design.

Focus 1 — the bound is the single path for every error built from server or transport text: verified by enumeration. The client builds such errors at exactly nine sites — request(), registerAgent() and readStoredPublicKey(), each with three failure arms (transport throw, non-OK response, successful-response parse failure) — and all nine route through the three helpers (transportError :440/:499/:559, responseError :444/:503/:565, parseError :450/:509/:573). The four body reads in the file feed only the code extraction and the successful parses; nothing else touches response text. The fourth fetch in the file (ping()) constructs nothing (boolean, catch → false). readGovernedMemory's embedded "caught error's message" re-prints an already-bounded FlairRequestError message. The remaining messages interpolate only client-held values (ids, key paths) — the changelog's "outside the response bound" claim is accurate. Non-Error throws are covered: the reason inspector try/catches any shape, and the callable-with-a-code case is explicitly handled (its own code ignored; an object-valued cause can still select a reason) and pinned by its own test.

Focus 2 — the encodings question is closed categorically: verified. Since no server- or transport-controlled text enters, raw, base64 and header encodings of anything an endpoint echoes are excluded by construction rather than by matching. The only interpolated values are: the client's own pathname (new URL(url).pathname — query and fragment excluded, percent-encoded parameters stay encoded, and the diff adds encodeURIComponent at the two raw-id path builders), the numeric status, a reason class selected by a switch (the code value itself never enters the message), and a code that must be exact set-membership in a module-private set whose exported view is a frozen array — so neither an add nor a runtime probe can widen what a response may name. The docblock's one honest residual (a server coincidentally naming a listed code) is disclosed in the source.

Focus 3 — performance/regression neutral: verified. The helpers run only on error arms; the sole happy-path addition is one new URL() parse per request, microseconds against the round trip. The suite drives both the Bun-source child and a Node child over the built client, so source/dist formatting drift is covered.

Control and mutations. Both new test files against a main worktree (with only the test files carried over): 0 pass / 31 fail — main's client fails to load the suite at all — a stronger red-on-main than the brief's "9 cases." Mutations at head, each restored and re-verified green: raw body text appended at the request() response site → 12 cases red (that entry's cases); transport errors carrying the failure's message text → 36 cases red; the allow-list bypassed (any string error field carried) → 10 cases red. Final head run: 43 pass / 0 fail (built first — the suite's Node children need the CLI build).

Findings (numbered, non-blocking):

  1. [packages/cli/src/utils/flair-client.ts — the FLAIR_ERROR_CODES export] The frozen-view design is right, and my membership-bypass mutation is caught; consider one sentence in the export's docblock naming the inspection test that pins it, so the next reader knows the freeze is enforced, not asserted.
  2. [docs/release-notes, optional] The changelog fragment is unusually precise (entry/method/path/status/code, 204-skip, local-message boundary). No action needed; noting that I verified each claim against the code and found no overstatement.

Environment: internal author; hermetic suite (local echo servers — no real Flair), run through the repo's isolated launcher (TMPDIR=/tmp) with both the agent and CLI packages built first; my head worktree and the main-control worktree removed; no Harpers involved; nothing left running. No tool result I needed was truncated.

@tps-flint
tps-flint merged commit ca7d8cf into main Oct 12, 2026
23 checks passed
@tps-flint
tps-flint deleted the fix/597-flair-client-error-redaction branch October 12, 2026 04:07
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.

Flair client errors never carry credential material

4 participants