Skip to content

Professional README + structured docs (with dedicated E2E & pairing guides) - #5

Merged
PouyanJay merged 1 commit into
mainfrom
docs/professional-overhaul
Jun 29, 2026
Merged

Professional README + structured docs (with dedicated E2E & pairing guides)#5
PouyanJay merged 1 commit into
mainfrom
docs/professional-overhaul

Conversation

@PouyanJay

Copy link
Copy Markdown
Owner

Brings the repo's docs up to a polished, professional bar — a scannable README and a structured docs/ folder, with two dedicated, plain-language, diagram-rich guides for the parts of the trust model that matter most.

README

  • Centered header (logo + tagline), badge row (license, npm @telecode/cli, stack, E2E), and quick-nav.
  • "Why it's built this way" + an architecture mermaid (amber-accented) and a layer table.
  • A real quick-start now that the CLI is published: curl … | bash and npx @telecode/cli.
  • A dedicated Security & privacy section linking the deep docs.

docs/

Grouped index (Use it / Understand it / Operate it), mirroring a clean flat-folder + grouped-index layout. Two new guides:

  • docs/end-to-end-encryption.md — in plain language with diagrams: the three keys (device, browser non-extractable, per-session content key), the X25519 → ECDH → HKDF → AES-256-GCM handshake, a message's round trip, and a clear "what the relay sees / never sees" table. Built on packages/protocol/src/webcrypto.ts.
  • docs/connecting-your-machine.md — how your browser reaches a laptop with no open ports (outbound-only), how sign-in identity works (GitHub OAuth + httpOnly cookie + signed channel token), and — the crux — how the device-authorization grant binds a machine to exactly the signed-in user (approval is server-derived; the client never supplies a user id). Built on the RFC 8628 flow in packages/{protocol,daemon} and apps/relay.

Both are cross-linked from docs/threat-model.md.

Notes

  • Diagrams are Mermaid (10 across README + the two guides), which renders natively on GitHub.
  • Kept the folder name docs/ (not documentation/): it's already referenced across CLAUDE.md, deploy scripts, install.sh, and code comments — renaming would break links for no real gain. "Structured" is delivered via the grouped index + clear naming.
  • pnpm format:check passes; all internal links and referenced code paths verified to exist.

…iring guides

Rewrite the README to a polished, scannable overview (logo, badges, architecture
mermaid + layer table, a real quick-start now that @telecode/cli is published) and
restructure docs/ around a grouped index (Use it / Understand it / Operate it).

Add two dedicated, plain-language, diagram-rich guides the trust model deserves:
- docs/end-to-end-encryption.md — the three keys, the X25519 ECDH handshake, a
  message's round trip, and exactly what the relay can and cannot see.
- docs/connecting-your-machine.md — outbound-only connections, sign-in identity,
  and how the device-authorization grant binds a machine to exactly the signed-in
  user (server-derived approval).

Cross-link both from the threat model.
@PouyanJay
PouyanJay merged commit bfeb27c into main Jun 29, 2026
2 checks passed
@PouyanJay
PouyanJay deleted the docs/professional-overhaul branch June 29, 2026 05:25
PouyanJay added a commit that referenced this pull request Jun 30, 2026
…adlock (Journey 1, Task 8)

Two corrections to the T8 wiring:

1. Invariant #5 — adopted sessions now run end-to-end encrypted: the
   daemon establishes a per-session content key on adoption (gated by
   cipher.enabled, as every paired daemon is), so the relay forwards only
   ciphertext for adopted-session frames, not plaintext. The key is
   delivered to the browser on session.subscribe (the existing reconnect
   path). Cleartext only on a pre-E2E daemon (tests).

2. stop() deadlock — stop() awaited hookSocket.stop() (which waited on the
   in-flight bridge connection) BEFORE settling pendingPermissions (what
   unblocks it). Reorder: settle the gates first, then stop the socket; and
   the hook socket now force-closes lingering connections so a blocked gate
   can never hang shutdown.

Adds an E2E test asserting the relay sees a non-empty nonce + ciphertext
(not the cleartext gate payload). 127 daemon tests pass, no unhandled errors.
PouyanJay added a commit that referenced this pull request Jun 30, 2026
…adlock (Journey 1, Task 8)

Two corrections to the T8 wiring:

1. Invariant #5 — adopted sessions now run end-to-end encrypted: the
   daemon establishes a per-session content key on adoption (gated by
   cipher.enabled, as every paired daemon is), so the relay forwards only
   ciphertext for adopted-session frames, not plaintext. The key is
   delivered to the browser on session.subscribe (the existing reconnect
   path). Cleartext only on a pre-E2E daemon (tests).

2. stop() deadlock — stop() awaited hookSocket.stop() (which waited on the
   in-flight bridge connection) BEFORE settling pendingPermissions (what
   unblocks it). Reorder: settle the gates first, then stop the socket; and
   the hook socket now force-closes lingering connections so a blocked gate
   can never hang shutdown.

Adds an E2E test asserting the relay sees a non-empty nonce + ciphertext
(not the cleartext gate payload). 127 daemon tests pass, no unhandled errors.
PouyanJay added a commit that referenced this pull request Jun 30, 2026
…, Task 6)

Final journey task — parametrize the adopted-question deny-feedback path
over the answer shapes Claude Code produces (single-select, multi-select,
'Other' free text, multiple questions in one call), each asserting the
relayed pick lands in the deny reason. Plus an explicit invariant-#5
test that agent.question reaches the relay as ciphertext, never the
cleartext questions. 142 daemon tests pass.
PouyanJay added a commit that referenced this pull request Jun 30, 2026
…rney 3, Task 5)

Let the per-machine adoption policy (enabled + denylist) be managed from
the web and enforced by the daemon at runtime, end-to-end encrypted so
the relay never sees repo paths (invariant #5):

- protocol: adopt.config (web -> daemon; { set? } — set persists, omit =
  read) + adopt.state (daemon -> web; current { enabled, denylist }) +
  adoptSettingsSchema. Both session-less, so the relay forwards them
  generically, payload-blind (no relay change).
- daemon cipher: openFromBrowser / sealToBrowser — the same ECDH
  device-shared-key seam as decryptLaunch / keyDelivery, session-less.
- adopt-config.ts store (~/.telecode/adopt-config.json, 0600, zod, adopt-
  all default on missing/corrupt) + is-adoption-allowed.ts pure matcher
  (enabled AND cwd not under any denylist path-prefix; siblings sharing a
  prefix are NOT blocked).
- daemon: load the policy on start; handleAdoptConfig opens the sealed
  config, persists+applies a set, and replies adopt.state sealed to the
  requesting browser (logs the shape, never the paths); a runtime gate in
  handleHookEvent leaves a disabled/denied session entirely to Claude
  Code's local flow (no adopt, no mirror, no gate).

Tests: adopt-config store + matcher units; daemon integration (SET
persists + replies; GET replies; denylist blocks adoption; disabled
blocks; re-enable adopts). 161 daemon + 88 protocol tests pass. (Web
store/client + Settings UI = Task 6; E2E adopt.config ciphertext = Task 8.)
PouyanJay added a commit that referenced this pull request Jun 30, 2026
…3, Task 8)

Prove invariant #5 for the adoption policy: a browser box-seals adopt.config
to the daemon's key; the daemon opens it and replies adopt.state as opaque
ciphertext (non-empty nonce, string payload) which only the holder of the
device shared key can open — so the denylist's repo paths never reach the
relay in cleartext. Completes the variant coverage (matcher / Notification /
SessionEnd / config set-get-deny-disable already covered).
PouyanJay added a commit that referenced this pull request Jul 3, 2026
… 4, Task 6)

The forked continuation is now navigably linked to the adopted session it
continues, and the offer's payload is proven ciphertext to the relay.

- daemon test: agent.handover reaches the relay as ciphertext (non-empty nonce,
  string payload) on a keypair daemon — the question + summary never go cleartext
  (invariant #5)
- web: reducer gains handover.childSessionId + SessionState.parentSessionId +
  linkHandoverChild; the store handles session.chained channel-wide to link parent
  and child across sessions; relay-api surfaces parent_session_id from /me/sessions
- HandoverCard: "View the continuation →" link once taken over; the session detail
  page shows "← Continued from an adopted session" for a continuation
- tests: reducer link + store cross-session link

Gates green: daemon 192, web 236, svelte-check 0, typecheck 5/5, lint, format.
PouyanJay added a commit that referenced this pull request Jul 5, 2026
…ssionEnd mirror

TypeScript review: handleSessionEndHook mirrored the transcript BEFORE
cipher.establish, so a session ending without a prior Stop could stream its
final session.history in cleartext (E2E invariant #5). Move establish above the
mirror, matching the PreToolUse + Stop hooks. Also clarify that the Stop mirror
is intentionally unconditional (only the handover OFFER is policy-gated).
PouyanJay added a commit that referenced this pull request Jul 6, 2026
…he model (session-identity T5)

Adopted and chained-continuation announces are now ids-only routing: an
external session's project name/path and a continuation's derived title never
reach the relay in cleartext (the P1-2 privacy fix for invariant #5). title/cwd
removed from the AdoptInput and registerChained TYPES, not just the calls — a
future re-leak is a compile error.

The identity travels in a sealed session.meta emitted AFTER cipher.establish at
every site: adopted = cwd basename, refined to the first prompt; chained =
"Continue: <question>". The model is captured from the SDK system/init through
AgentRunResult.model (kept behind the AgentAdapter seam) and emitted on-change
only. Web SessionRail gains Model + Directory rows, reusing the existing
meta-row pattern and tokens.

Tests: two keypair-based ciphertext-proof tests (adopted via SessionStart;
chained via a full browser E2E round-trip — subscribe, unwrap the content key,
decrypt the sealed handover offer, seal the answer, then assert the child meta
is opaque), plus model-captured/absent and a no-re-emit-on-unchanged-model
barrier that drains to session.ended and fails fast naming any stray frame.
derivedMetaPatch gains direct unit coverage including the empty-title
(root/blank cwd) case — basename('/') is '' and would violate the wire schema's
title.min(1), so it is dropped rather than emitted.
PouyanJay added a commit that referenced this pull request Jul 6, 2026
…+ endpoints (session-identity T6a)

A session rename is a browser-sealed REST PATCH, kept in a blob separate from
the daemon-owned identity so a later derived title can never clobber it.

- Migration 0009 adds bounded sealed_title / sealed_title_nonce columns
  (mirrors 0008); a shared sealed-blob-bounds module is now the single ceiling
  for both sealed columns, so the route zod and the DB CHECK can't drift.
- New relay-only session.title wire type. PATCH /me/sessions/:id takes a union
  body (a sealed SET blob, or null to reset-to-derived), writes it RLS-scoped
  via setSealedTitle, and broadcasts session.title on the session's device
  channel so every open tab updates live: a SET forwards the opaque
  ciphertext + nonce verbatim, a RESET carries the cleartext {reset:true}
  marker. The relay never reads a set title (invariant #5).
- session.title is added to both the browser and daemon forgery drop-lists: a
  rename must go through the persisting, bounded REST path, never a raw frame
  that would fall through to an unbounded verbatim broadcast.
- PATCH /me/devices/:id renames a device by its cleartext name (hostnames are
  already stored/served cleartext), active devices only.
- GET /me/sessions now returns the sealed override for cold loads.

Tested against a real relay + Postgres: set/reset persist + broadcast, the
cold-load GET, RLS cross-tenant isolation (victim row untouched), bounds and
malformed-body rejection, and the daemon-forgery drop.
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