From f4a345d5a3301bd3e645936c783022bcdf2f59d9 Mon Sep 17 00:00:00 2001 From: Logix Date: Fri, 28 Aug 2026 16:29:27 -0400 Subject: [PATCH 1/4] Expand AGENTS.md with security and mobile details Add details about browser delegation, Hub mobile home, and connection security. --- AGENTS.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 05f19a7..7ecafdd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,7 +147,19 @@ The shared React renderer supports desktop and direct browser clients. New rende Remote-access credentials are authority boundaries. Pairing tokens must remain short-lived, memory-only, and single-use. Persist only device-session digests in owner-only atomic stores; enforce expiry and revocation. Browser HTTP/WebSocket origins must match exactly or be explicitly configured—never reflect arbitrary `Origin`/forwarded headers. Keep authentication limiters bounded, and do not hardcode CJ's `crewcode.logixhub.icu` deployment as a default Hub URL. -The self-hosted Hub is a separate `crewcode hub` process, not Electron renderer state. Keep its SQLite store owner-only and server-side; persist WebAuthn public credentials and only digests of browser/CSRF secrets. Bootstrap credentials and WebAuthn challenges stay short-lived and memory-only. Require user verification, exact configured RP origin/id, one-use challenges, secure HttpOnly SameSite cookies, and CSRF checks for mutations. Do not let Hub identity imply brain execution authority: enrollment, tickets, relay, and brain-side authorization remain separate gates. +Browser delegation keeps its agent-facing endpoint Brain-loopback and bearer-scoped; +delegate requests, editor watches, and LSP handles remain bound to the authenticated +browser session that owns them. Browser plugin iframes load only approved plugin +assets through expiring asset-only grants and continue to invoke capabilities through +the trusted renderer plus manifest permission gate. Remote GitHub UI may drive the +Brain's `gh` device login and registered-workspace publishing, but must never expose +the Brain's GitHub credential or allow remote logout. + +The self-hosted Hub is a separate `crewcode hub` process, not Electron renderer state. `crewcode hub --local-brain` may spawn a sibling `crewcode brain` on the Hub host after owner passkey setup; keep Hub SQLite and Brain credentials in separate data dirs, do not default-grant scopes, and still enroll extra machines with `crewcode enroll` then `crewcode brain`. Keep its SQLite store owner-only and server-side; persist WebAuthn public credentials and only digests of browser/CSRF secrets. Bootstrap credentials and WebAuthn challenges stay short-lived and memory-only. Require user verification, exact configured RP origin/id, one-use challenges, secure HttpOnly SameSite cookies, and CSRF checks for mutations. Machine enrollment tokens must also stay short-lived, memory-only, single-use, and rate-limited; persist only machine bearer digests at the Hub and keep the brain credential file owner-only. Presence and relay connections are outbound-only and revocation must fail closed. Hub connection tickets remain short-lived, memory-only, one-shot, browser-session/user/machine bound, and exact-origin protected. Relay application frames must stay end-to-end encrypted and ordered; the Hub may route metadata but must not receive RPC/source/terminal/agent plaintext. Do not let Hub identity, machine presence, or requested ticket scope imply Brain execution authority: `crewcode brain` defaults to no RPC grants, and every decrypted method must pass both explicit Brain-local scope and registered-workspace validation. Relay loss means pending outcomes are interrupted, never successful. + +Remote cross-thread conversation handoff stays Brain-local. Namespace browser replay shards under `web:`; never copy the replay store into browser persistence. Require an authenticated owner-held destination bridge and Brain-local `agent` scope, refuse handoff while the destination is running, perform bounded disposable summarization on the Brain, clear the destination native resume id, and replay the combined destination history exactly once on its next native-provider prompt. Missing history, lost ownership, or summary failure is an explicit failure, never inferred success. + +The Hub mobile home is control-plane-only until a user selects an online machine. At ≤768px an authenticated Hub root may route to `/app?hub=mobile`, where `MobileDashboard` reads only cookie-authenticated Hub session and machine-presence data. Do not install a Brain runtime, request relay authority, expose fake agent/worktree statistics, or affect Electron/direct-server/desktop startup from this route. Machine selection enters `/app?hub=mobile&machine=…`, which may open a disposable end-to-end encrypted, Brain-scoped mobile overview; it must request only the scopes needed for real stats, render unavailable values explicitly, return bounded recent-thread metadata rather than transcript bodies, and close its relay before the full renderer opens. Compatibility fallback for an older Brain may use `transcripts.mtimes` to render untitled saved rows, but must never use `transcripts.loadAll` for this overview. Mobile overview counts must use the canonical Mission Control `deriveMissionStats` aggregation over Brain-visible transcript sessions and executions; do not duplicate `mc-stats` semantics, and do not classify completed solo turns as done. A recent-row deep link must carry a bounded workspace/tab/scope descriptor, validate workspace→tab→scope ownership after transcript hydration, preserve the exact transcript scope id when restoring a missing browser session, and refuse invalid/conflicting descriptors. Only the explicit full-app action enters `/app?machine=…` without a thread target. Keep `/?hub-admin=1` as the mobile escape hatch for Hub account and device administration. ### Path alias From 99bd67e1a8c089d8423788bfea47973e50b79a66 Mon Sep 17 00:00:00 2001 From: Logix Date: Fri, 28 Aug 2026 16:31:16 -0400 Subject: [PATCH 2/4] Enhance security model documentation Expanded the security model section detailing the Hub owner to enrolled machine boundary, connection and execution gates, tests, and residual limitations. --- docs/security-model.md | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/docs/security-model.md b/docs/security-model.md index 025280a..32b6b86 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -59,6 +59,82 @@ per-authenticated-session RPC bandwidth/request budget, remote execution custody not fully persisted, and public deployment still depends on correctly configured TLS/reverse-proxy infrastructure. Prefer loopback or a trusted tailnet. +## Hub owner -> enrolled machine boundary + +**Boundary:** signing into the Hub must not silently enroll a machine or grant remote +execution. A stolen/replayed enrollment link, database disclosure, cross-site +request, or revoked machine credential must not produce lasting machine presence. + +**Enforcement:** only a valid owner browser session plus its rotated CSRF secret can +issue an enrollment token. Tokens contain 256 bits of random secret, live only in +Hub memory, expire after ten minutes, are single-use, and are consumed after a failed +secret presentation for a known id. Issuance/enrollment requests are bounded by a +per-peer limiter. Enrollment creates an Ed25519 identity on the brain and a separate +256-bit bearer credential; the brain stores both in an owner-only atomic file while +the Hub stores the public key and only SHA-256 of the bearer secret. Heartbeats are +outbound HTTPS requests. Presence goes offline after 90 seconds without a successful +heartbeat, and owner revocation immediately makes the bearer credential fail closed. +Browser mutations retain exact-origin and CSRF enforcement. The explicitly labeled +first-owner QR carries the same short-lived, single-use bootstrap fragment already +printed to the trusted terminal; it expires in ten minutes and is removed after +registration. Normal mobile QR codes encode only the stable validated HTTPS Hub +origin and are rendered in the authenticated dashboard; they contain no browser +session, enrollment secret, or Brain ticket. Phone-approved enrollment keeps its +Ed25519 private key on the PC, uses the short code only for human comparison, and +protects polling/credential delivery with a separate 256-bit request secret. Pending +requests are memory-only, bounded, rate-limited, ten-minute, owner-approved with +CSRF, one-time on delivery, and audited. +Tailscale setup refuses to overwrite an existing Serve configuration without an +explicit replacement flag. QR transfer does not weaken normal passkey sign-in. + +**Connection and execution gates:** only an authenticated Hub browser session plus +CSRF can issue a 60-second memory-only ticket for one owned, active, relay-connected +machine. Ticket ids are one-shot even after a wrong-secret guess. The authenticated +outbound Brain WebSocket and exact-origin browser WebSocket are paired only for that +machine; the relay bounds frame size and buffered output, applies a shared per-connection +token bucket (240-frame/8 MiB burst, refilling at 60 frames and 2 MiB per second), +and never accepts arbitrary destinations. Browser and Brain then use ephemeral P-256 ECDH. The Brain signs the +handshake transcript with its enrolled Ed25519 key, and ordered application frames +use direction-separated HKDF/AES-256-GCM keys. The Hub sees routing metadata and +handshake public values, but not RPC, source, terminal, prompt, or response plaintext. + +Hub identity still does not grant execution. `crewcode hub --local-brain` enrolls +the Hub host only after the owner passkey exists, then spawns a sibling Brain; +it does not skip Brain-local scope checks or write workspace roots into Hub SQLite. +The first `crewcode brain` start grants +no RPC scope by default and seeds an owner-only persisted policy from explicit local +`--workspace-root` and repeatable `--allow-scope` settings. Thereafter Settings → +Brain Access manages it only through E2EE owner RPC. Reductions apply immediately, +stop affected agents/terminals, and remove scopes from existing sessions; additions +require a fresh ticket and handshake. Each decrypted method must be included in both +the ticket request and current local grant. The backend revalidates live workspace +roots for filesystem, Git, PTY, attachments, and agent calls. + +**Tests:** `hub-server.test.ts` covers CSRF, issue/enroll, local-brain owner gating, +replay rejection, stale +presence, heartbeat, and revocation. `hub-local-brain.test.ts` covers sibling spawn +plans, loopback origin matching, credential reuse, and supervisor stop. `hub-machine-enrollment.test.ts` covers URL +policy, argument-secret avoidance, credential validation, owner-only file mode, and +Brain CLI grants. `hub-relay.test.ts` covers ticket expiry/one-shot behavior, +authenticated relay routing, Ed25519-authenticated E2EE, scoped read success, local +scope denial, per-connection traffic rejection, and ticket replay rejection. +`hub-relay-client.test.ts` covers explicit fresh-ticket reconnect without RPC replay. + +**Residual limitation:** machine credential rotation/logout and recovery are not yet +implemented. Relay traffic is bounded per connection, but durable bandwidth metrics +and broader aggregate abuse accounting are not implemented. Browser relay loss now +detaches Brain-owned terminals and agents instead of stopping them. A fresh encrypted +connection can explicitly reclaim stable resource ids, with up to 100 owned resources +per user and 1,000 events / 1 MiB of detached evidence buffered per resource; +interrupted RPCs are never replayed. +Execution custody is still process-resident rather than crash durable: Brain process, +VPS, revocation, or persistent Brain-to-Hub relay loss can stop execution without a +complete remote halt journal. Attachments are tunneled as ordered chunks inside the +E2EE relay: Hub receives ciphertext only, while Brain enforces `workspace:write`, a +25 MiB file limit, registered-root and symlink containment, strict sequence and +size bounds, SHA-256 integrity, active-upload limits, and temporary-file cleanup. + + ## Hop 1 — untrusted content -> agent **Boundary:** injected instructions in scraped/file/MCP content must not gain From 9afe600c4b29cf0efe3052ecdaed7a7c28e79c4a Mon Sep 17 00:00:00 2001 From: Logix Date: Fri, 28 Aug 2026 16:33:17 -0400 Subject: [PATCH 3/4] Add brain entry to input configuration --- electron.vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 48246e7..9533de0 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -25,6 +25,7 @@ export default defineConfig({ index: resolve('src/main/index.ts'), headless: resolve('src/main/headless.ts'), hub: resolve('src/main/hub.ts'), + brain: resolve('src/main/hub-machine-enrollment.ts'), }, }, } From 81cc6e5040993163e90dd14caafeee5c6fe6160b Mon Sep 17 00:00:00 2001 From: Logix Date: Fri, 28 Aug 2026 16:36:38 -0400 Subject: [PATCH 4/4] Refactor canvas mode pane rendering logic --- .../src/components/canvas/CanvasMode.tsx | 145 +++++++++++++----- 1 file changed, 108 insertions(+), 37 deletions(-) diff --git a/src/renderer/src/components/canvas/CanvasMode.tsx b/src/renderer/src/components/canvas/CanvasMode.tsx index 606c8bf..323eb4e 100644 --- a/src/renderer/src/components/canvas/CanvasMode.tsx +++ b/src/renderer/src/components/canvas/CanvasMode.tsx @@ -95,43 +95,114 @@ export function CanvasMode({ workspaceName, openChatCount, openTerminalCount, pa ) : (
- {panes.map(pane => ( -
-
- {pane.title} -
- {pane.kind === 'chat' && pane.modePrompt && ( - - )} - {pane.kind === 'chat' && ( - + {panes.map(pane => { + const isChat = pane.kind === 'chat' + const menuOpen = paneMenuId === pane.id + return ( +
+
+ {pane.title} + {isMobile ? ( + // Phone: collapse the action cluster into a single + // overflow button. The popover carries prompt toggle, + // verbose-log toggle, and close. Add chat / Add terminal + // live on the page-level FAB. +
+ + {menuOpen && ( +
+ {isChat && pane.modePrompt && ( + + )} + {isChat && ( + + )} + +
+ )} +
+ ) : ( + // Desktop: keep the full inline cluster (mode prompt, + // logs toggle, Add chat, Add terminal, close). +
+ {isChat && pane.modePrompt && ( + + )} + {isChat && ( + + )} + + + +
)}
{pane.content}