feat(canvas): add shared Codex sessions, SSH accounts, browser control, and nested groups - #112
Conversation
The hook token authenticates a session, not which node — and three open PRs now rest on itCross-cutting review note, posted on both #112 and #113 because neither can fix this alone and the fix is the same in both. #98 sits on the same ground. The mechanism
So the token proves "some session nodeterm spawned is calling". It proves nothing about which node is calling. Any control-capable agent can put a different node's id in the body and the server has no way to tell. That was tolerable while a forged What it buys in each PR#113 — per-pair trust. #112 — thread ownership, and through it, browser control. The browser router does the right thing: it demands #98 — same ground, smaller blast radius. Scope, stated plainlyAll of this lives inside one OS user. Nothing crosses the user boundary: the hook server is loopback, the browser socket is 0600 in a 0700 dir, and an attacker who already has that uid can read the files directly. The realistic threat is a prompt-injected or misbehaving agent moving laterally to a sibling node — which is precisely the boundary #112's browser scoping and #113's per-pair grants are advertised to enforce. The fixBind the token to the node, and stop reading identity from the body:
That is one change in Two smaller things worth folding in while that seam is open, both from #112: What I verified, and what I did notRead and confirmed in the code: the single token and its identical injection ( Not verified by running: no crafted POST was actually sent, and neither the bind→browser chain nor the grant spoof was reproduced end to end. Both are read off the code, and a multi-account setup with a live browser node is what would settle them. If either of you sees a check I have missed on the path from a forged body to the acting node, I would rather be wrong about this. Credit where due: #112's cross-account resolution genuinely fails closed (duplicate thread ids resolve ambiguous unless one inode proves otherwise), its rollout hardlinking is carefully defended, and #113 fixes a real bug — the 2 s slowloris guard was killing confirmation-gated |
cb80f65 to
c2b0b99
Compare
|
Addressed the #112 thread-ownership/browser-control path in 1a73cfc. The shared hook bearer can no longer choose the acting Codex node through a request body:
This uses a keychain-protected root secret plus deterministic per-node capabilities rather than persisting a token map in the endpoint file. That preserves node identity across an Electron relaunch without putting node capabilities in the shared endpoint handoff. The focused negative coverage includes missing/wrong node capabilities, mismatched body node ids, tampered persisted ownership, relay forwarding, and protected-secret failure. Typecheck, build, 177-test expanded regression coverage, a final 69-test ownership/relay run, and diff-check pass. This closes the #112 bind-to-victim chain and the related observed-title spoof. It does not claim to change the separate generic /control/* identity seam discussed for #113. |
Route each NodeTerm Codex client's existing node id, hook endpoint, and canvas-control gate through thread-scoped shell_environment_policy overrides while retaining one persistent --remote unix:// app server. Fail closed before connecting when identity is missing or invalid. Cover concurrent launch/resume mappings and rejected assignments with isolated fixtures.
ae5f03d to
3b09ff2
Compare
|
Updated after rebasing onto current New in the PR head:
Post-rebase verification: typecheck passed, build passed, 12 changed-scope test files / 194 tests passed, and |
… to the OS pasteboard Slice 1 of PR eneskirca#112, cut by us out of integration/codex-112 (the PR's content merged onto main, de-Prettier-ed, semantic conflicts resolved in main's favour). These three UX micro-fixes have no coupling to the Codex shared-identity work that makes up the rest of that PR, so they land on their own. **Trackpad vs mouse wheel** (`canvas/wheel-gesture.ts`) — Chromium reports a macOS two-finger trackpad scroll and an ordinary mouse wheel as the same unmodified pixel-wheel, so `settings.wheelZoom` (mouse-first zoom) used to take scroll-to-pan away from trackpad users too. `MacWheelGestureRouter` tells the two apart by the legacy `wheelDeltaY` notch and the smoothness of the deltas, and the classification is STICKY for the length of one physical gesture: Chromium can quantize a later trackpad/momentum packet to exactly 120, and treating that one packet as a mouse notch is what produced a one-frame zoom in the middle of an otherwise pure pan. `panOnScroll` is now always on for macOS so React Flow's own pan handles what the router routes to it; native scrollers (`.nowheel` — terminal, Monaco, markdown) keep scrolling themselves either way. **Canvas image paste/drop** (`canvas/canvas-image-import.ts`, `terminal/file-drop.ts`) — dropping or pasting an image on empty canvas opens it as an image preview node, reusing the existing terminal path resolver (`localPathsForFiles`, extracted from `droppedPaths`) and the existing Open-file node path. A Finder drop keeps its real path; clipboard/browser bytes are written to the managed uploads dir first. Paste is armed by the last pointer press landing on the pane itself, so a Cmd+V aimed at a panel or a keyboard-opened dialog can't spawn a node behind it, and an async resolve is abandoned if the project changed underneath it. **Cmd-C on file-backed nodes** (`canvas/canvas-file-copy.ts`, `main/clipboard-files.ts`) — with no text selection, Cmd/Ctrl+C now puts the selected editor/video/web nodes' files on the macOS pasteboard as FILE REFERENCES, so Finder pastes the actual files. Paths cross an IPC trust boundary, so main re-checks them: absolute only, capped, de-duplicated, stat'ed, and all-or-nothing so a multi-selection is never silently truncated. Remote (sshFs) and missing files are excluded. Also fixes a defect in the sliced test: `wheel-gesture.test.ts`'s 'does not manually pan over non-terminal native scrollers' duplicated the previous test's assertion and asserted the opposite of its name. It now actually exercises a non-terminal native scroller (native destination) and the gesture continuing off it (flow-pan) — the sticky-classification case nothing else covered. Co-Authored-By: Corvin <corvin@streamlain.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Group frames may now contain other frames, to any depth, and the sessions sidebar renders that tree instead of one flat level. Transforms (src/renderer/state/workspace.ts, all pure + unit-tested): - `groupSelectedNodes` wraps objects sharing ONE container — frames included — creating the wrapper inside that container. Mixed containers and an ancestor selected with its own descendant are refused, not scrambled. - `ungroupNodes` promotes children into the frame's OWN parent (not the root). - `reparentNode` moves a node or a whole frame subtree, keeps its ROOT-space position fixed, and refuses a cycle. - new `selectedRootIds`, `addSelectionToGroup`, `reorderGroupWithinParent`. - `groupsFirst` is now topological (depth-first from the root); a flat sort is not enough once two frames compare equal. `nodeStatesToFlow` uses it, so the persisted order is parent-first at any depth. Fixes the geometry bug the pure arithmetic hides: a wrapper created at (minX-28, minY-62) relative to its new parent is routinely negative and, with `extent:'parent'`, React Flow clamps it into an inverted range — the frame snaps hundreds of px away and drags the whole wrapped subtree with it. Every ancestor frame is re-fitted around the new wrapper, pinned by a test that asserts the frame FITS (and fails without the fix). Sidebar disclosure moves to a persisted `settings.sidebarCollapsedItems` (projects AND frames), replacing the "a project switch resets manual toggles" effect; `sidebarAutoCollapse` now only supplies the default for an untouched project row. Keys are pruned on every write against the live tree. CLAUDE.md documents the new contract. `projectHeadClickAction`, the project working badge and `projectIdAtIndex` are untouched. Also: a frame's body is `pointer-events: none` with the label pill as its `dragHandle` (a frame is a background container, not a giant drag target, and an outer frame must not swallow clicks meant for one drawn inside it), worktree/cwd inheritance and the Source Control scope walk the ancestor chain, and the canvas-control `group`/`ungroup`/`move` verbs + docs follow. Slice 3 of PR eneskirca#112, sliced by us. Co-Authored-By: Corvin <corvin@streamlain.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(startup): avoid keychain access at boot when no device is paired (from #112)
feat(ssh): host attachment — a remote node inside a local project (extracted from #112)
…n-codex fallback Many Codex canvas nodes now share ONE `codex app-server` instead of each spawning its own process tree, and each node keeps a stable identity (its thread) across resumes, in-pane restarts and app restarts. It also closes a real hole: the hook server's shared bearer proves only "some nodeterm session sent this", never WHICH one, so a caller could bind its own codex thread to a SIBLING node. Each `/codex-thread/*` route now additionally requires a per-node HMAC capability, minted from a keychain-backed secret and injected only into that node's session env. The launcher FALLS BACK rather than dying: every failure path execs plain `codex` with the caller's arguments intact, and reports the reason so the node shows a `plain codex` chip and the first fallback raises a toast. Sliced from PR eneskirca#112 by Corvin. Co-Authored-By: Corvin <corvin@streamlain.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refreshPushIdentity() called loadOrCreateKeyPair() before checking whether any phone is paired, so every boot touched macOS Safe Storage. On a locally-signed development build that raises a Keychain ACL prompt even though there is no paired destination to notify. Check loadApprovedDevices() first and return early when nothing is paired. Behaviour is unchanged: push-notify's resolveTarget already requires `id && id.hasPairedPhone`, so an identity with hasPairedPhone=false was never selected for host mode anyway. Extracted from eneskirca#112 (integration/codex-112); the browser-use-backend.ts half of the original commit is unrelated and not included. (cherry picked from commit 210d3b4) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Status update, so this isn't sitting here unexplained. Not merging this as a unit — 18,671 lines across 115 files spanning four unrelated features is not reviewable as one change, and it has been landing in slices instead. Where each slice stands: Already on Measured and dropped: the shared-Codex-identity work ( Blocked on decisions I owe you, not on the code: Superseded: The one piece nobody has triaged
The catch is that its whole reason to exist is delivering a message on a schedule, so it sits directly on top of If you would rather not carry this branch while that happens, closing it is reasonable — the extracted slices are already on |
…owser guest browserGuests stored whatever integer arrived over IPC. Harmless while the map only routed new-window events; a privilege escalation the day that integer selects a webContents we attach a debugger to — an unvalidated id can name the app's own window. Guard stolen from PR #112's S8 backend (getType() !== 'webview'), with credit to proteus-dev; the node id is validated too, because it is a map key here and a storage key later and it comes from git-shared project.json. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
Several NodeTerm surfaces assumed one Codex process, one login, one machine, and one direct app-server connection. That breaks down when Codex nodes share persistent app servers to save RAM or when sessions live on an SSH host:
The change also fills canvas UX gaps around terminal sharpness, image transfer, macOS gestures, node framing, nested groups, persistent inter-agent messaging, and recurring agent tasks.
Shared Codex identity and lifecycle
Machine-scoped Codex accounts and SSH sessions
Node-authenticated thread ownership
Browser control
Inter-agent mailbox and Loop automation
send,reply, andstatuscommands for existing NodeTerm agent nodes.Canvas, terminal, and file UX
autodefault to keep terminal text sharp after drag operations; explicit renderer settings remain available and Linux/Windows defaults are unchanged.Nested groups and Sessions sidebar
Safety properties
Verification
npm run typecheck: passed after rebasing onto currentmain.npm run build: passed after rebasing onto currentmain.git diff --check: passed.No release publishing, production deployment, Developer ID signing, notarization, or release-pipeline dispatch is part of this PR.