Skip to content

feat: typed auth handshake, capability negotiation, inbound validation (protocol P0, 1/2) - #102

Merged
saucam merged 1 commit into
mainfrom
feat/protocol-handshake-validation
Jul 5, 2026
Merged

feat: typed auth handshake, capability negotiation, inbound validation (protocol P0, 1/2)#102
saucam merged 1 commit into
mainfrom
feat/protocol-handshake-validation

Conversation

@saucam

@saucam saucam commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #101 (base = feat/extract-protocol-package). Merge #101 first; I'll retarget this to main after. Part 1 of the protocol P0 hardening from the world-class-protocol audit; part 2 (seq-based resume + send idempotency) follows.

All changes are additive — no PROTOCOL_VERSION bump; legacy clients (Rust TUI, older web) work unchanged.

1. Typed auth handshake + capability negotiation

The first frame of every connection ({type:"auth", token}) was hand-parsed in server.ts and absent from the typed protocol entirely. Now:

  • AuthMsg is part of @codeoid/protocol, with optional protocolVersion, capabilities[], and client (diagnostics).
  • auth.ok returns the daemon's capabilities — negotiation is now bidirectional (previously the daemon could never know what a client supports).
  • Published CAPABILITIES vocabulary: parts, replay.chunked, replay.resume, send.idempotency (the latter two land in part 2). Unknown capability strings are ignored, never rejected.
  • Web client declares parts + replay.chunked; the daemon logs the negotiated pair per connection.

2. Runtime validation of the whole inbound surface

The daemon did parsed as ClientMessage — a bare cast, no runtime check. Now:

  • New @codeoid/protocol/schemas subpath export: Zod schemas for AuthMsg + all 24 ClientMessage variants, with parseClientMessage / parseAuthMsg helpers. zod is an optional peer dependency — type-only consumers of @codeoid/protocol stay dependency-free.
  • Daemon validates every frame: unknown fields are stripped (forward-compat: a newer client's additive fields must never get it rejected — regression-tested), unknown message types and out-of-bounds payloads get invalid_request, malformed auth frames close 4001.

3. Published input LIMITS (the token-bill safety net)

session.send.text previously had no size cap — the only bound was the 16 MiB frame limit, so an accidental multi-megabyte paste would flow straight into the model's context and burn real money in a single turn. LIMITS are now published in the protocol (clients can pre-validate) and enforced in the schemas: SEND_TEXT_MAX (1M chars), name/path/query/model/id caps, attachment count + per-field caps, and contentdata mutual-exclusion with datamimeType requirement.

Tests (45 new)

  • Compile-time coverage assertion: schema discriminants ⇔ ClientMessage variants, both directions — adding a message type without a schema (or vice versa) fails tsc.
  • Per-variant round-trip fidelity (sample map keyed by type, so a new variant without a sample is a compile error).
  • Forward-compat regression: unknown fields stripped not rejected (incl. on the auth frame), unknown types rejected.
  • LIMITS boundary cases (at-cap accepted / over-cap rejected), attachment exclusivity, auth handshake matrix.
  • CI scripts extended so packages/protocol is linted, typechecked (tsc -p), and tested.

Verification

Full daemon suite 831 pass · web 166 pass · root+package typecheck clean · biome clean · bun run build green.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added protocol capability negotiation during app startup, with the client and server now exchanging supported features.
    • Expanded protocol validation support for incoming messages and handshake data.
  • Bug Fixes

    • Rejected malformed or unsupported messages earlier, preventing invalid requests from reaching app logic.
    • Improved error responses for bad input with clearer request-level feedback.
  • Chores

    • Updated release and publishing checks to cover the protocol package more consistently.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds Zod-based runtime validation for inbound protocol messages in packages/protocol (schemas.ts, tests, exports), introduces capability/limits/auth-handshake types, wires the daemon to validate auth frames and messages via the new parsers with capability negotiation, updates the web client to send capability metadata, and adjusts build/lint/test/publish scripts.

Changes

Protocol Schema Validation and Capability Negotiation

Layer / File(s) Summary
Capability, limits, and auth types
packages/protocol/src/types.ts
Adds CAPABILITIES, Capability, LIMITS, AuthMsg interface, and extends AuthOkMsg with optional capabilities.
Zod schemas and parsing helpers
packages/protocol/src/schemas.ts, packages/protocol/src/schemas.test.ts
Implements per-message Zod schemas, attachmentSchema, discriminated clientMessageSchema, authMsgSchema, ParseResult<T>, parseClientMessage/parseAuthMsg, with extensive test coverage for fidelity, limits, and rejection cases.
Daemon auth and message validation wiring
src/daemon/server.ts
Adds SERVER_CAPABILITIES, extends SocketData with protocolVersion/capabilities, validates auth frames via parseAuthMsg (closing socket on invalid auth), includes capabilities in auth.ok, and validates inbound messages via parseClientMessage, returning invalid_request errors on failure.
Web client capability negotiation
web/src/protocol/types.ts, web/src/lib/ws.ts
Adds CAPABILITIES and AuthOkMsg.capabilities to web protocol types; updates the auth frame sent on WebSocket open to include protocolVersion, capabilities, and client.
Build/publish and package export config
package.json, packages/protocol/package.json
Extends lint/format/test/typecheck scripts to cover packages/protocol/src, adds prepublishOnly script, adds ./schemas export, excludes test files from published files, and adds optional zod peer dependency.

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

Sequence Diagram(s)

sequenceDiagram
  participant WebClient as Web Client (ws.ts)
  participant Daemon as Daemon Server
  participant AuthParser as parseAuthMsg
  participant MsgParser as parseClientMessage

  WebClient->>Daemon: auth frame (token, protocolVersion, capabilities, client)
  Daemon->>AuthParser: parseAuthMsg(frame)
  AuthParser-->>Daemon: ok / error
  alt invalid auth
    Daemon-->>WebClient: close socket
  else valid auth
    Daemon->>Daemon: verifyToken, store SocketData
    Daemon-->>WebClient: auth.ok (capabilities: SERVER_CAPABILITIES)
  end
  WebClient->>Daemon: client message
  Daemon->>MsgParser: parseClientMessage(message)
  MsgParser-->>Daemon: ok / error
  alt invalid message
    Daemon-->>WebClient: response.error (invalid_request)
  else valid message
    Daemon->>Daemon: route to session logic
  end
Loading

Possibly related issues

Possibly related PRs

  • saucam/codeoid#48: This PR's Zod schema validation covers the usage.daily client message that PR #48 introduced end-to-end.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: typed auth handshake, capability negotiation, and inbound validation.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/protocol-handshake-validation

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

Protocol P0 hardening, part 1 of 2 (all additive — no version bump):

- Type the auth handshake: `AuthMsg` is now part of @codeoid/protocol (the
  first frame of every connection was previously undocumented). Clients may
  declare `protocolVersion` + `capabilities`; `auth.ok` now returns the
  daemon's `capabilities`, making version/feature negotiation bidirectional.
  CAPABILITIES vocabulary published (parts, replay.chunked, replay.resume,
  send.idempotency).

- Runtime validation of the entire inbound surface: new
  `@codeoid/protocol/schemas` subpath export (zod as an OPTIONAL peer dep —
  type-only consumers stay dependency-free). The daemon previously cast
  `parsed as ClientMessage` with no runtime check; it now validates every
  frame — unknown fields are STRIPPED (forward-compat preserved), unknown
  message types and out-of-bounds payloads are rejected with
  invalid_request, malformed auth frames close 4001.

- Published input LIMITS, enforced in schemas — notably SEND_TEXT_MAX
  (1M chars) as a token-bill safety net: session.send text goes straight
  into the model's context, so an accidental multi-megabyte paste would
  burn real money in one turn. Plus name/path/query/model/id caps and
  attachment count/size caps with content⊕data mutual-exclusion.

- Web client sends the enriched auth frame (version, parts +
  replay.chunked capabilities, client name); mirror types updated.

- CI scripts now lint/typecheck/test packages/protocol.

Tests: 45 new — compile-time schema↔union coverage assertion, per-variant
round-trip fidelity, forward-compat strip-not-reject regression, LIMITS
boundary cases, attachment exclusivity, auth handshake cases. Full suite
831 pass; web 166 pass; build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@saucam
saucam force-pushed the feat/protocol-handshake-validation branch from 05ec861 to f32f7f8 Compare July 5, 2026 08:05
@saucam
saucam changed the base branch from feat/extract-protocol-package to main July 5, 2026 08:05
@saucam saucam closed this Jul 5, 2026
@saucam saucam reopened this Jul 5, 2026
@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.34%. Comparing base (8c5b728) to head (f32f7f8).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #102      +/-   ##
==========================================
+ Coverage   75.86%   76.34%   +0.47%     
==========================================
  Files          69       70       +1     
  Lines       11312    11539     +227     
==========================================
+ Hits         8582     8809     +227     
  Misses       2730     2730              
Flag Coverage Δ
daemon 76.34% <100.00%> (+0.47%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/protocol/src/schemas.ts 100.00% <100.00%> (ø)
packages/protocol/src/types.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
web/src/protocol/types.ts (1)

11-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

CAPABILITIES duplicated verbatim from @codeoid/protocol.

This object is identical to packages/protocol/src/types.ts's CAPABILITIES. Maintaining two copies risks silent drift (e.g., a capability added on one side but not mirrored on the other, breaking negotiation).

Consider re-exporting from @codeoid/protocol instead:

-export const CAPABILITIES = {
-  PARTS: "parts",
-  CHUNKED_REPLAY: "replay.chunked",
-  SEQ_RESUME: "replay.resume",
-  SEND_IDEMPOTENCY: "send.idempotency",
-} as const;
+export { CAPABILITIES } from "`@codeoid/protocol`";

Please confirm the web app can take a dependency on @codeoid/protocol (it's a plain const, no zod involved) before applying.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/protocol/types.ts` around lines 11 - 23, The CAPABILITIES constant in
the web protocol types is duplicated from `@codeoid/protocol` and should be
removed to avoid drift. Update the web side to re-export or import CAPABILITIES
from `@codeoid/protocol` in the types.ts module, and verify the web app can depend
on that package since it is just a plain const. Keep the existing CAPABILITIES
symbol available to callers so auth handshake code continues to reference the
shared source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@web/src/protocol/types.ts`:
- Around line 11-23: The CAPABILITIES constant in the web protocol types is
duplicated from `@codeoid/protocol` and should be removed to avoid drift. Update
the web side to re-export or import CAPABILITIES from `@codeoid/protocol` in the
types.ts module, and verify the web app can depend on that package since it is
just a plain const. Keep the existing CAPABILITIES symbol available to callers
so auth handshake code continues to reference the shared source of truth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 20759a47-1291-4cff-9d3e-631a45b57e6d

📥 Commits

Reviewing files that changed from the base of the PR and between 8c5b728 and f32f7f8.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock, !**/*.lock, !bun.lock
📒 Files selected for processing (8)
  • package.json
  • packages/protocol/package.json
  • packages/protocol/src/schemas.test.ts
  • packages/protocol/src/schemas.ts
  • packages/protocol/src/types.ts
  • src/daemon/server.ts
  • web/src/lib/ws.ts
  • web/src/protocol/types.ts

@saucam
saucam merged commit d5a12b3 into main Jul 5, 2026
5 checks passed
@saucam

saucam commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

Re CodeRabbit's nitpick (CAPABILITIES duplicated in web/src/protocol/types.ts): acknowledged but deliberately not applied here. The duplication concern is real, but the entire web/src/protocol/types.ts file is a documented hand-maintained mirror (see its header) — web is a separate package with its own lockfile and standalone bun install --frozen-lockfile in CI, and is not a workspace member, so it can't take a workspace: dependency on @codeoid/protocol today. Migrating web onto the package (which eliminates the whole mirror, not just this one const) is the explicitly-deferred follow-up called out in #101; importing a single symbol now would leave one file split between two sources of truth. Drift risk in the interim is bounded: capability strings are additive and ignore-unknown by contract, and the daemon-side vocabulary is the single wire truth.

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