Status tracker for the security hardening plan. Source: brainstorming session (2026-03-18).
See PROGRESS.json for detailed validation. All 10 tasks done, 131 tests pass.
- T01: Default bind 0.0.0.0 → 127.0.0.1 (
src/index.tsx) - T02:
src/env-safety.ts— buildShellEnv() allowlist (PATH/HOME/TERM/TZ/LANG/USER) - T03: shell.ts wired to buildShellEnv (no more
...process.env) - T04: sandbox.ts both bwrap sites wired to buildShellEnv
- T05:
src/url-safety.ts— extracted validateUrl + BLOCKED_RANGES from web.ts - T06: A2A tools (a2a_discover, a2a_call) validate URLs for SSRF; registered agents bypass
- T07: "a2a"+"scratchpad" added to TOOL_DOMAINS; createAgentServers uses isToolEnabled for both
- T08: ~/.claude mounted read-only in TUI sandbox (was rw)
- T09: Removed process.env.BRAVE_API_KEY fallback in web.ts
- T10: Tests for env-safety (7 tests) and url-safety (13 tests)
- DNS rebinding / TOCTOU — validateUrl resolves DNS once, fetch resolves again → attacker can flip
- IPv4-mapped IPv6 bypass —
::ffff:127.0.0.1not caught by regex - Decimal/octal IP encoding —
0x7f000001,2130706433could bypass regex
Depends on Wave 1. ~2 weeks estimated.
| Task | Files | Description |
|---|---|---|
| W2-T01 | New src/credentials.ts, src/manifest.ts |
CredentialStore class + frontmatter schema (~150 lines) |
| W2-T02 | src/tools/index.ts |
Wire CredentialStore into createAgentServers (~50 lines) |
| W2-T03 | src/agent.ts PreToolUse hook |
Audit logging on credential resolution (~50 lines) |
| W2-T04 | New src/egress-proxy.ts |
Egress proxy with undici ProxyAgent (~150 lines) |
| W2-T05 | src/tools/web.ts, src/tools/a2a.ts |
Wire egress proxy into web tools (~30 lines) |
| W2-T06 | src/index.tsx |
Headless run subcommand (extends --message) (~60 lines) |
| W2-T07 | src/index.tsx |
runs.jsonl logging in run mode (~20 lines) |
| W2-T08 | src/access.ts, src/agent.ts |
Per-user tool deny in access.yaml + canUseTool (~40 lines) |
| W2-T09 | src/agent.ts canUseTool |
canUseTool operation allowlist for billing agent (~30 lines) |
| W2-T10 | src/tools/index.ts |
Restrict external MCP in serve mode (~20 lines) |
| W2-T11 | New CLI code | credentials migrate CLI command (~80 lines) |
| W2-T12 | docs/ | Docs: credentials.yaml format reference + agent authoring guide |
| W2-T13 | New test files | Tests for CredentialStore + egress proxy (~100 lines) |
- CredentialStore: wraps
Record<string,string>+ frontmatter policy..resolveFlat(domain)returns subset,.toFlatEnv()returns all (legacy fallback). - Tool identity: credentials scoped by MCP server domain name (key in TOOL_DOMAINS). Each createXxxTools() receives a resolver scoped to its domain.
- Credential flow:
.env(dotenvx) + frontmattercredentialspolicy →loadAgentEnv()→CredentialStore→store.resolveFlat("web")→ only granted keys returned. - Backward compat: No
credentialsin frontmatter → all keys available (existing behavior).credentialspresent → strict mode. - Egress: undici ProxyAgent injected into web tool fetch calls. Local forward proxy enforces domain allowlist. Node.js
fetch()ignores HTTP_PROXY — must use undici dispatcher. - Shell zero-cred: buildShellEnv already handles this (Wave 1).
- Headless run: extends existing --message with structured exit codes, runs.jsonl logging.
credentials:
grants:
braintree-read:
keys: [BRAINTREE_MERCHANT_ID, BRAINTREE_PUBLIC_KEY]
tools: [web]
email:
keys: [POSTMARK_SERVER_TOKEN]
tools: [web]
sensitive:
keys: [WIRE_ACCOUNT_NUMBER]
tools: [web]
approval: requiredsandbox:
shell: false
allowedDomains:
- api.braintreegateway.com
- "*.supabase.co"
- api.postmarkapp.com
- api.anthropic.com- CredentialStore resolves only granted keys per tool domain
- Ungranteed keys return empty for strict-mode agents
- Legacy agents (no credentials config) work unchanged
- Egress proxy blocks requests to non-allowlisted domains
- Egress proxy allows requests to allowlisted domains
- A2A tool respects egress allowlist (not just SSRF)
-
mastersof-ai run billing "test"exits with structured code - runs.jsonl contains entry after headless run
- User with tools.deny:["shell"] cannot use shell
- canUseTool blocks Braintree write operations for billing agent
- External command-based MCP servers blocked in serve mode
- Credential audit log captures resolution events
Wave 3 is not a code wave — it's deploying a specific agent instance on private infrastructure. No changes to the open source harness are needed. The security waves (1–2, 4–8) built the generic runtime; Wave 3 exercises it for a private use case.
Tasks (all private ops, not repo changes):
- Configure agent IDENTITY.md (shell disabled, egress allowlisted, credentials granted)
- Set up Tailscale networking (join tailnet, tag server, configure ACLs)
- Serve mode behind Tailscale
- E2e validation of credential isolation and egress control
- Cron setup for headless runs
- Deployment guide (private docs)
See PROGRESS.json for detailed validation. All 9 tasks done, 203 unit tests pass.
- IPv4-mapped IPv6 hex-short bypass:
new URL()normalizes[::ffff:127.0.0.1]to::ffff:7f00:1—normalizeIpnow handles both dotted and hex-short forms - DNS pinning breaks HTTPS: URL hostname rewriting causes TLS SNI mismatch — reverted to validate-then-fetch; DNS pinning deferred to undici dispatcher (future)
- Protocol validation: Added
http:/https:check — rejectsfile://,data://,ftp:// - DNS null check:
dns.lookupresult now null-checked before use - web_search content tags: Search results now wrapped in
<fetched_content>tags like web_fetch
| Task | Files | Description |
|---|---|---|
| W4-T01 | New src/content-safety.ts, web.ts, agent.ts |
Web fetch content boundaries (structural tags + system prompt) (~80 lines) |
| W4-T02 | src/tools/web.ts |
Extraction model default in serve mode (~20 lines) |
| W4-T03 | src/agent.ts:416-420 |
Memory content tagging (CONTEXT.md as untrusted) (~15 lines) |
| W4-T04 | src/url-safety.ts |
DNS rebinding / redirect hardening + IPv6 + IP encoding (~100 lines) |
| W4-T05 | src/a2a/server.ts |
A2A server authentication (~40 lines) |
| W4-T06 | src/serve.ts |
Deprecate WS query param token (~20 lines) |
| W4-T07 | src/serve.ts:177 |
Drop raw token from connectedClients (~5 lines) |
| W4-T08 | src/agent-context.ts |
Per-user stderr logging (~10 lines) |
| W4-T09 | docs/ | Docs: security model documentation (~3 pages) |
Must address all three SSRF hardening gaps:
- DNS rebinding / TOCTOU: Pin resolved IP for the actual connection. Validate IP post-resolve before connecting. Consider using undici's
connectoption or a customlookupfunction that caches and re-validates. - IPv4-mapped IPv6 bypass: Normalize
::ffff:x.x.x.xaddresses to their IPv4 equivalent before checking BLOCKED_RANGES. Check both the raw and normalized forms. - Decimal/octal/hex IP encoding: Normalize IP representations (0x7f000001 → 127.0.0.1, 2130706433 → 127.0.0.1, 0177.0.0.1 → 127.0.0.1) before range checking. Use
new URL()normalization + explicit parsing.
- Fetched web content wrapped in
<fetched_content>tags - System prompt contains untrusted content instruction
- Memory content tagged as
<memory_context> - Redirect to internal IP blocked by DNS rebinding defense
- IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) blocked
- Decimal/octal/hex IP representations blocked
- A2A server rejects unauthenticated requests
- WS query param token logs deprecation warning
- connectedClients Map does not contain raw token
- Per-user log files created for remote sessions
See PROGRESS.json for detailed validation. 9 implementation tasks + 15 review-fix tasks, 238 tests pass.
- W5-T01:
src/ipc-protocol.ts— discriminated union IPC types with type guards - W5-T02:
src/session-worker.ts— child process entry point (fork per session, SDK query isolation) - W5-T03:
src/serve.ts+src/worker-manager.ts— handleMessage dispatches via IPC, WorkerManager lifecycle - W5-T04: Per-worker env injection via fork env option (buildShellEnv + ANTHROPIC_API_KEY passthrough)
- W5-T05:
src/agent-context.ts— per-user proposalsDir:state/{agent}/proposals/{userId}/ - W5-T06:
src/query-mutex.ts— per-user concurrent query serialization - W5-T07:
src/access.ts—generateAccessToken()for partner token generation - W5-T08:
src/session-worker.test.ts— 23 tests (IPC, mutex, tokens, proposalsDir) - W5-T09:
docs/partner-onboarding.md— full partner onboarding guide
15 fixes applied from review findings:
| Fix | Sev | Source | Description |
|---|---|---|---|
| P0-1 | CRIT | Eng | Worker exit code 0 → promise never settles → mutex locked forever. Fixed: settled flag + safeResolve/safeReject |
| P0-3 | CRIT | Eng | process.send throws ERR_IPC_CHANNEL_CLOSED → infinite recursion. Fixed: try/catch in send() helper |
| P1-1 | HIGH | Eng | Dangling SIGKILL timer in kill(). Fixed: killTimer on state, cleared in exit handler |
| P1-2 | HIGH | Eng | QueryMutex broken under 3+ concurrent waiters (while-loop TOCTOU). Fixed: FIFO queue pattern |
| P1-3 | HIGH | Eng | Worker allows concurrent handleMessage. Fixed: guard rejects if activeQuery !== null |
| P1-4 | HIGH | Eng | unhandledRejection handler doesn't exit. Fixed: process.exit(1) |
| P2-2 | MED | Eng | stdio line-buffering garbles logs. Fixed: "inherit" instead of pipe |
| P2-4 | MED | Eng | Missing tsx loader in forked workers. Fixed: execArgv: safeExecArgv |
| P2-6 | MED | Eng | 100ms shutdown too short. Fixed: 5s timeout + shuttingDown flag |
| F1 | HIGH | Sec | Fork bomb — re-subscribe orphans workers, no max cap. Fixed: kill prev + maxWorkers cap (20) |
| F2 | HIGH | Sec | IPC frame type not validated → WS injection. Fixed: allowlist of known frame types |
| F3 | HIGH | Sec | execArgv leaks --inspect → debug port RCE. Fixed: filter --inspect/--debug flags |
| F5 | MED | Sec | Raw token in HTTP rate limiter key. Fixed: hashToken() before use |
| F7 | MED | Sec | Roster broadcast leaks all agents to all users. Fixed: per-user filtered broadcast |
| A1 | IMP | Arch | buildSystemPrompt re-parses manifest every message. Noted: manifest cached at init, full fix deferred to Wave 6 |
- IPC messages round-trip through JSON serialization
- Worker spawned per conversation, killed on WS disconnect
- Worker crash sends error to WebSocket (non-zero exit rejects promise)
- Worker exit code 0 settles the result promise (P0-1)
-
process.sendfailure does not crash the worker (P0-3) - Worker env contains only safe base vars + agent credentials
- Per-user proposalsDir isolated (state/{agent}/proposals/{userId}/)
- Concurrent queries serialized by FIFO queue mutex (P1-2)
- 3+ concurrent waiters execute in strict FIFO order
- Worker rejects concurrent handleMessage (P1-3)
- Re-subscribe kills previous worker (F1)
- maxWorkers cap enforced (F1)
- IPC frame type validated against allowlist before WS relay (F2)
- execArgv filtered — no --inspect/--debug in workers (F3)
- Rate limiter key uses hashed token (F5)
- Roster broadcast filtered per user access (F7)
- generateAccessToken produces unique, cryptographically random tokens
- Partner onboarding documented end-to-end
See SECURITY-WAVES-5.1-5.2.md for detailed validation tasks. 5 tasks covering integration tests for all 15 review fixes. Must pass before merge.
See PROGRESS.json for detailed validation. All 9 tasks done, 264 tests pass (263 pass, 1 skip).
| Task | Files | Description | Source |
|---|---|---|---|
| W6-T01 | New src/sdk-stream.ts, src/session-worker.ts, src/components/App.tsx |
SDK stream processing shared abstraction — eliminates as any duplication between worker and TUI (~200 lines new, ~100 removed each) |
Arch #2 |
| W6-T02 | src/session-worker.ts, src/serve.ts |
Double error on worker init failure — single error path, no duplicate messages to client (~15 lines) | Arch #5 |
| W6-T03 | src/query-mutex.ts, src/serve.ts, tests |
QueryMutex timeout — acquire(key, timeoutMs) throws on timeout, waiter removed from queue (~30 lines) |
Sec F4 |
| W6-T04 | src/access.ts, src/serve.ts |
Token revocation safeCompare — export and use timing-safe comparison for hash check (~10 lines) |
Sec F6 |
| W6-T05 | src/serve.ts or new src/ws-protocol.ts |
WebSocket message schema validation — Zod schema for WsClientMessage, reject invalid shapes (~50 lines) | Sec F8 |
| W6-T06 | src/serve.ts, src/worker-manager.ts |
Worker ready timeout — kill worker + reject if no "ready" within 30s (~25 lines) | Sec F10 |
| W6-T07 | src/serve.ts |
Pending approval cleanup on worker crash — clear map, send rejection frames to client (~15 lines) | Eng P2-3 |
| W6-T08 | src/config.ts, src/serve.ts |
Configurable serve.maxWorkers in HarnessConfig — default 20, config.yaml override (~15 lines) |
Eng P3-3 |
| W6-T09 | src/health.ts, src/serve.ts |
Worker pool size in /health/deep — workerPool: { active, max, utilization } (~20 lines) |
Arch #3 |
- W6-T09 depends on W6-T08 (needs maxWorkers in config to report the cap)
- All others are independent
- SdkStreamProcessor used by both session-worker.ts and App.tsx
- Worker init failure produces exactly one error to client
- Mutex acquire with timeout throws and releases correctly
- Token revocation uses timing-safe comparison
- Malformed WS messages rejected with structured error
- Worker ready timeout kills stuck workers within 30s
- Worker crash clears pending approvals and notifies client
-
serve.maxWorkersconfigurable via config.yaml -
/health/deepreports worker pool utilization
See PROGRESS.json for detailed validation. All 21 tasks done, 282 tests pass.
Incorporates all deferred findings from Wave 6 security/engineering/architecture reviews, plus CLI DX. Organized into four groups: stream processor refinements, type system hardening, worker/serve robustness, and observability.
- W7-T09 depends on W7-T06 (safeSend helper needed for worker message relay)
- W7-T14 depends on W7-T13 (health prune uses same timer pattern)
- All others are independent
| Task | Files | Description | Source |
|---|---|---|---|
| W7-T01 | src/sdk-stream.ts, src/components/App.tsx, src/session-worker.ts, tests |
Rename tool_block_stop → content_block_stop — event fires for ALL content block types (text, tool, thinking), not just tools. Rename the event kind, update both consumers. App.tsx already guards on inToolUseRef, so behavior is unchanged. |
Sec L1, Eng P2-4 |
| W7-T02 | src/sdk-stream.ts |
Document kind discriminant choice — add comment explaining why kind (not type) is used, to prevent a future contributor from "fixing" it to match IPC/WS conventions. |
Arch #8 |
| W7-T03 | src/sdk-stream.ts, src/session-worker.ts |
Fix tool_input_delta toolId extraction — contentBlock?.id may not exist on delta events. Return null when toolId would be empty instead of producing orphan frames. Session-worker.ts should track the active toolId from tool_use_start and use it for input deltas. |
Sec L2, Eng P2-5, Arch #3 |
| W7-T04 | src/session-worker.ts |
Promote frameId to module scope — currently resets to 0 per message, breaking reconnection replay after the first turn. MessageBuffer.since(lastMessageId) returns nothing when IDs restart. Make it a persistent counter across messages. |
Sec L3 |
| W7-T05 | src/session-worker.ts |
Guard result send after error — on query error, worker sends both an error IPC and a result IPC. The settled flag makes this harmless, but it's wasteful. Add a hadError flag to skip the result send. |
Eng P3-11 |
-
content_block_stopis the event kind name (nottool_block_stop) - App.tsx and session-worker.ts handle
content_block_stopcorrectly -
sdk-stream.tshas comment explainingkindvstypechoice -
tool_input_deltareturnsnullwhen toolId is absent (not empty string) - Session-worker.ts tracks active toolId from
tool_use_startfor input delta frames -
frameIdis module-scoped in session-worker.ts, monotonically increasing across messages - Worker does not send result IPC after sending error IPC
| Task | Files | Description | Source |
|---|---|---|---|
| W7-T06 | src/ws-protocol.ts, src/types/ws.ts |
Single source of truth for WS client types — add compile-time assertion that Zod schema output matches the TypeScript WsClientMessage union. If they diverge, the build fails. Remove the as WsClientMessage cast. |
Arch #1 |
| W7-T07 | src/session-worker.ts, src/components/App.tsx |
Exhaustive switch enforcement — add default: { const _: never = event; } to both SdkEvent switch statements so new event kinds cause compile errors if unhandled. |
Arch #5 |
| W7-T08 | src/serve.ts, src/ipc-protocol.ts |
Extract ALLOWED_FRAME_TYPES to module scope — move from the handleMessage closure to a module-level const in ipc-protocol.ts, co-located with the frame type definitions. Makes it auditable and avoids per-call reconstruction. |
Arch #6 |
| W7-T09 | src/serve.ts |
Eliminate residual as any cast — messageBuffer.push(frame as any) contradicts W6-T01's goal. After the frame passes ALLOWED_FRAME_TYPES, narrow the type precisely (e.g., `as WsToken |
WsToolUseStart`). |
- Build fails if Zod schema and
WsClientMessageunion diverge - No
as WsClientMessagecast in ws-protocol.ts - Both SdkEvent switch statements have exhaustive
nevercheck - Adding a new SdkEvent kind without handling it causes a compile error
-
ALLOWED_FRAME_TYPESlives inipc-protocol.tsat module scope - No
as anycasts in serve.ts messageBuffer.push
| Task | Files | Description | Source |
|---|---|---|---|
| W7-T10 | src/serve.ts |
safeSend wrapper — create safeSend(ws, data) helper that wraps ws.send(JSON.stringify(data)) in try/catch. Replace bare ws.send calls in post-processing, error paths, and approval cleanup. Prevents cascading throws on closed sockets. |
Eng P2-6 |
| W7-T11 | src/worker-manager.ts, src/session-worker.ts |
Minimize worker config exposure — send only the config subset the worker needs (model, effort, tools, hooks, logging level) instead of the full HarnessConfig. Reduces information exposure if a worker is compromised. |
Sec L6 |
| W7-T12 | src/ws-protocol.ts |
Schema tightening — add .max(200_000) to message content (belt-and-suspenders with rate limiter's maxMessageLength). Add .max(Number.MAX_SAFE_INTEGER) to lastMessageId. |
Sec INFO-1, Eng P3-8 |
| W7-T13 | src/access.ts |
safeCompare length-safety — document that the function is only constant-time for equal-length inputs (all current callers use fixed-length SHA-256 hex). Add JSDoc note. |
Sec L4 |
-
safeSendused in all post-processing ws.send calls in handleMessage -
safeSendused in approval cleanup and error paths - Worker receives only needed config subset (no
serve.rateLimits,serve.privacy) - WS
contentfield has max length in Zod schema -
lastMessageIdhas explicit max in Zod schema -
safeComparehas JSDoc documenting equal-length-only guarantee
| Task | Files | Description | Source |
|---|---|---|---|
| W7-T14 | src/health.ts |
Bounded health arrays — replace unbounded errors/successes arrays with a prune-on-insert strategy (every 1000 insertions, trim entries older than 1 hour). Prevents memory growth under sustained load without deep health checks. |
Sec L5, Arch #9 |
| W7-T15 | src/worker-manager.ts, src/health.ts |
WorkerManager.getStats() method — localize the utilization computation inside WorkerManager instead of spreading it across the serve.ts lambda. HealthMonitor callback becomes () => workerManager.getStats(). |
Arch #7 |
| W7-T16 | src/session-worker.test.ts |
Wave 6 test coverage gaps — add tests for: (a) worker ready timeout behavior, (b) approval cleanup on WS close, (c) maxWorkers edge values (0, -1, NaN, string), (d) WS schema edge cases (agentId > 200 chars, negative lastMessageId). | Eng P3-12 |
| W7-T17 | CLI code | mastersof-ai credentials check --agent <name> — validate agent credentials config, report which keys are granted/missing. |
CLI DX |
| W7-T18 | CLI code | mastersof-ai access create --name <name> --agents <list> — generate token, append to access.yaml. |
CLI DX |
| W7-T19 | CLI code | mastersof-ai status <agent> — read runs.jsonl, show recent headless run results. |
CLI DX |
| W7-T20 | CLI code | mastersof-ai preflight --agent <name> — validate full config: agent exists, credentials present, egress allowlist valid, sandbox config correct. |
CLI DX |
| W7-T21 | src/access.ts |
Token rotation mechanism — generate new token for existing user, update access.yaml, disconnect old sessions. | CLI DX |
- Health arrays never exceed ~2000 entries (prune at 1000 threshold)
- No memory growth under sustained load without deep health calls
-
workerManager.getStats()returnsWorkerPoolStats - Health monitor lambda is just
() => workerManager.getStats() - Ready timeout test: covered by W6 (worker-manager.ts readyTimer)
- Approval cleanup test: covered by W6-T07 (serve.ts workerExitHandler)
- maxWorkers test: 0 → default(20), -1 → clamped to 1, NaN → default
- WS schema test: agentId > 200 chars rejected, negative lastMessageId rejected
-
credentials checkreports granted/missing keys per domain -
access creategenerates token and appends to access.yaml -
statusshows recent run results from runs.jsonl -
preflightvalidates agent config end-to-end - Token rotation generates new token, revokes old, disconnects sessions
-
content_block_stopevent kind used consistently - Zero
as anycasts in stream processing and WS relay paths - Zod↔TypeScript type drift causes build failure
- Exhaustive switches enforce handling of all SdkEvent kinds
-
safeSendeliminates all bare ws.send in error-prone paths - Worker config minimized to needed subset
- Health arrays bounded, worker stats localized
- Test suite covers all Wave 6 failure modes
- CLI commands operational and documented
- All existing tests continue to pass (282 tests, 0 failures)
See PROGRESS.json for detailed validation. All 8 tasks done, 308 tests pass.
| Task | Files | Description |
|---|---|---|
| W8-T01 | DESIGN.md, docs/ | Architecture refresh — update for Waves 1–7 modules |
| W8-T02 | CLAUDE.md | Update quick orientation for new modules (env-safety, url-safety, credentials, egress-proxy, content-safety, ipc-protocol, session-worker, worker-manager, query-mutex, sdk-stream, ws-protocol) |
| W8-T03 | CHANGELOG.md | Version history for Waves 1–7 |
| W8-T04 | docs/security.md | Full security narrative for audit |
Three independent reviews (security, engineering, architecture) identified four items that are correct but improvable. Deferred from Wave 7 to avoid scope creep during the security wave.
| Task | Files | Description | Source |
|---|---|---|---|
| W8-T05 | src/serve.ts |
Extend safeSend to handleSubscribe and WS message handler — convert remaining ~19 bare ws.send(JSON.stringify(...)) calls outside handleMessage to use safeSend. Currently protected by surrounding try/catch or early-lifecycle guarantees, but inconsistent with the pattern established in W7-T10. |
Sec #1, Eng #2, Arch #2 |
| W8-T06 | src/serve.ts |
Type guard for bufferable frames — replace frame as unknown as WsToken | WsToolUseStart with a proper type guard function (isBufferableFrame(frame): frame is WsToken | WsToolUseStart) that validates the frame shape at runtime. Currently safe because ALLOWED_FRAME_TYPES + type check guard the path, but the as unknown as cast bypasses TypeScript's structural checks. |
Sec #8, Eng #4, Arch #4 |
| W8-T07 | src/ws-protocol.ts, src/types/ws.ts |
Zod-derived WsClientMessage type — use z.infer<typeof WsClientMessageSchema> to derive the TypeScript type from the Zod schema, making it the single source of truth. Eliminates the bidirectional assertion (W7-T06) and catches optional field drift. Requires rewriting the WsClientMessage union in types/ws.ts to be a re-export of the inferred type. |
Eng #8, Arch #4 |
| W8-T08 | src/index.tsx or new src/cli/ |
CLI subcommand extraction — extract credentials check, access create, access rotate, status, preflight, run, and credentials migrate from index.tsx into src/cli/*.ts modules with a dispatcher. index.tsx is ~730 lines with a long chain of if (args[0] === ...) blocks. |
Arch #5 |
- W8-T06 depends on W8-T05 (safeSend must be complete before tightening frame types)
- W8-T07 is independent
- W8-T08 is independent
- All others are independent
- Zero bare
ws.send(JSON.stringify(...))calls remaining in serve.ts -
isBufferableFrame()type guard validates frame shape at runtime - No
as unknown asoras anycasts in serve.ts WS relay paths -
WsClientMessagetype derived from Zod schema viaz.infer<> - Adding an optional field to a Zod schema variant without updating the TS type causes a build failure
- No
as WsClientMessagecast in ws-protocol.ts - CLI subcommands live in
src/cli/*.tswith a dispatcher in index.tsx - index.tsx is <200 lines (dispatcher + arg parsing + shared helpers only)
- All existing tests continue to pass
- DESIGN.md reflects current architecture (Waves 1–7)
- CLAUDE.md quick orientation covers all new modules
- CHANGELOG covers all security waves
- Security narrative complete for external audit
- All deferred hardening items from Wave 7 reviews resolved
- Zero
as anyoras unknown ascasts in serve.ts WS relay paths - CLI commands modular and testable
See PROGRESS.json for detailed validation. All 16 tasks done, 282 tests pass (1 skip).
Fixes from 3 independent reviews (security, engineering, architecture) of Wave 8. Cross-validated across all three reviewers. 16 findings total: 3 HIGH, 7 MEDIUM, 6 LOW.
| Task | Files | Description | Sources | Consensus |
|---|---|---|---|---|
| W8.1-T01 | src/index.tsx |
Dispatcher fall-through — convert independent if blocks to if/else if chain or add early returns. Currently relies on process.exit() inside imported modules — if any module returns instead of exiting, execution falls through to TUI. |
Sec #1, Eng F1, Arch #1 | HIGH (2H, 1M) |
| W8.1-T02 | src/serve.ts |
Frame allowlist-output — construct new objects with only known fields before relaying IPC frames to WebSocket. Currently all ALLOWED_FRAME_TYPES frames relay verbatim including any extra properties a compromised worker injects. Apply to both bufferable and non-bufferable relay paths. | Sec #2, Eng #1, Arch #1 | HIGH (1H, 2M) |
| W8.1-T03 | src/cli/credentials.ts, src/cli/preflight.ts |
Remove as any casts in credential grant iteration — use the Zod-inferred grant type from manifest.ts instead of (grant as any).keys. Could mask schema changes in security CLI tools. |
Arch #6, Sec cross-val HIGH, Eng #4 | HIGH (1H, 2M) |
| Task | Files | Description | Sources | Consensus |
|---|---|---|---|---|
| W8.1-T04 | docs/security.md |
Fix stale _AssertZodMatchesTs reference — update to describe the z.infer<> derivation mechanism that replaced the bidirectional assertion. |
Sec #3, Eng F8, Arch #4 | MED (3M) |
| W8.1-T05 | src/serve.ts, src/types/ws.ts |
Type safeSend as WsServerMessage — add WsWarning type, add retryAfter? to WsError, add WsPong type. Prevents protocol drift at compile time. Pre-existing but Wave 8 should have caught it. |
Arch #2, Eng #2, Sec #8 | MED (1H, 1M, 1I) |
| W8.1-T06 | src/cli/run.ts |
Replace as any casts in streamToStdout — use extractSdkEvent() from sdk-stream.ts instead of raw (msg as any).event casts. Wave 6 built this abstraction for exactly this purpose. |
Arch #8, Eng #5, Sec cross-val LOW | MED (2M, 1L) |
| W8.1-T07 | docs/security-model.md |
Update or deprecate — either update to match security.md (add Layers 9-10, process isolation, WS protocol safety) or replace contents with a pointer to security.md. | Arch #7, Sec cross-val MED, Eng #6 | MED (3M) |
| W8.1-T08 | src/serve.ts |
Wrap ws.close() in try/catch at auth failure (line ~888) and rate limit (line ~916) paths — consistent with token revocation path. |
Eng F3, Sec cross-val HIGH, Arch cross-val LOW | MED (1H, 1M, 1L) |
| W8.1-T09 | src/index.tsx |
Unknown subcommand handling — check if args[0] is a recognized command before falling through to TUI. Print usage hint for unrecognized positional args. mastersof-ai access (missing subcommand) silently launching TUI is confusing. |
Arch #10, Sec cross-val MED, Eng #10 | MED (1M, 2L) |
| W8.1-T10 | src/index.tsx, src/cli/access.ts |
--agents explicit or warn — require --agents flag or print prominent warning when defaulting to wildcard *. Maximum access by default violates least-privilege. |
Sec #6, Arch cross-val MED, Eng #8 | MED (1M, 2L) |
| Task | Files | Description | Sources | Consensus |
|---|---|---|---|---|
| W8.1-T11 | src/serve.ts, src/ipc-protocol.ts |
Move isBufferableFrame to ipc-protocol.ts — co-locate with ALLOWED_FRAME_TYPES. Note: creates dependency from IPC to WS types. |
Arch #3, Sec cross-val MED, Eng LOW | LOW (1M, 1M, 1L) |
| W8.1-T12 | src/cli/access.ts |
Validate --name parameter — use validateName from path-safety at creation time, not first connection. |
Sec #5, Arch cross-val LOW, Eng #8 | LOW (3L) |
| W8.1-T13 | src/cli/preflight.ts |
Remove dead buildOptions import |
Sec #7, Eng #9, Arch #5 | LOW (3L) |
| W8.1-T14 | CLAUDE.md |
Add CLI subcommand examples to Running Locally section | Arch #11, Sec #8, Eng #11 | LOW (3L) |
| W8.1-T15 | src/types/ws.ts |
Add retryAfter? to WsError — symptom of T05, fix alongside it |
Sec #4, Arch #6 | LOW (covered by T05) |
| W8.1-T16 | — | Shared CLI context type — deferred. Not actionable at 10 modules. Revisit if CLI grows past 15 commands. | Arch #5, Sec DISAGREE, Eng LOW | LOW (deferred) |
- W8.1-T02 depends on W8.1-T11 if we move isBufferableFrame first (or do both together)
- W8.1-T05 and W8.1-T15 should be done together (safeSend typing + WsError field)
- All others are independent
- Dispatcher uses
if/else ifor early returns — no fall-through possible - IPC frames relay only known fields to WebSocket (no extra-property passthrough)
- Zero
as anycasts in credentials.ts, preflight.ts, run.ts -
safeSendtyped asWsServerMessage— protocol drift caught at compile time -
WsWarning,WsPongtypes added to WsServerMessage union -
ws.close()wrapped in try/catch at all call sites - Unknown subcommands print usage error instead of launching TUI
-
--agentsflag required or warns on wildcard default - docs/security.md describes
z.infer<>mechanism (no_AssertZodMatchesTsreference) - docs/security-model.md updated with Layers 9-10, process isolation
-
isBufferableFrameco-located withALLOWED_FRAME_TYPESin ipc-protocol.ts -
access create --namevalidated withvalidateName - Dead imports removed
- CLAUDE.md has CLI subcommand examples
- All existing tests continue to pass (282 pass, 0 fail, 1 skip)
- TypeScript compiles with zero errors