Skip to content

feat(canvas): add shared Codex sessions, SSH accounts, browser control, and nested groups - #112

Open
proteus-dev wants to merge 49 commits into
eneskirca:mainfrom
proteus-dev:codex/nodeterm-shared-codex-identity
Open

feat(canvas): add shared Codex sessions, SSH accounts, browser control, and nested groups#112
proteus-dev wants to merge 49 commits into
eneskirca:mainfrom
proteus-dev:codex/nodeterm-shared-codex-identity

Conversation

@proteus-dev

@proteus-dev proteus-dev commented Aug 10, 2026

Copy link
Copy Markdown

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:

  • a process-global node id cannot identify parallel canvas nodes;
  • resumes and account switches can lose canvas ownership, linked context, titles, or context-menu capabilities;
  • one system login cannot represent multiple isolated local and remote Codex accounts or their separate usage;
  • a local rollout is not automatically available to a Codex app-server on another machine;
  • a browser node can be opened by an agent, but without an owner-scoped Browser Plugin bridge the agent cannot reliably inspect, scroll, click, or type in it.

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

  • Reuse one Codex app-server per machine and account over Unix WebSockets instead of starting one server per node.
  • Carry a distinct NodeTerm node id and control endpoint through each CLI session; no process-global static node id.
  • Keep tmux sessions and shared app-servers alive when Electron quits, then reconnect nodes after restart.
  • Package the Codex relay as an Electron resource and deploy a bundled relay/launcher to connected SSH hosts; remote hosts need Node.js, Codex, and SSH, not a NodeTerm checkout.
  • Bind fresh, resumed, and in-TUI-resumed threads to their actual canvas node.
  • Resolve Codex-owned session titles and update node chrome after create/resume.
  • Create direct resumes as full Codex agent nodes with restart, account, linked-context, mailbox, and canvas-control capabilities.
  • Preserve legacy system-account mappings while failing closed on invalid, missing, duplicate, tampered, or cross-account ownership.
  • Close upgraded WebSocket clients during shutdown so relay clients cannot strand the server.

Machine-scoped Codex accounts and SSH sessions

  • Group account settings by machine: this Mac and each configured SSH host appear once, with their system and managed accounts listed below them.
  • Support isolated managed Codex logins on local and SSH machines, including remote device login, retry, removal, and system-account discovery.
  • Keep credentials and per-account app-servers on the machine that owns them.
  • Show the same account identity and Local/SSH provenance pills in Settings, node headers, create menus, and account-switch menus.
  • Let new local or remote Codex nodes select the appropriate system or managed account.
  • Switch accounts on the same machine by resuming the same conversation id rather than silently forking it.
  • Copy an idle local Codex conversation to a selected SSH account, preserve its conversation id, verify the remote app-server can discover it, and only then recycle that node onto the SSH host. The local rollout remains available.
  • Stage cross-machine rollout copies under a private remote directory, install atomically, and never overwrite an existing remote rollout.
  • Aggregate account-specific session selection and usage without mixing values between accounts; ambiguous ownership fails closed.
  • Prevent an explicitly selected missing account from falling back to another login.
  • Upload pasted/dropped files and screenshots to the owning SSH host before inserting their paths into remote terminals.
  • Give remote Codex nodes the same restart and account context-menu surfaces as local Codex nodes, scoped to accounts on their host.

Node-authenticated thread ownership

  • Give each Codex node a distinct HMAC capability instead of allowing the shared hook bearer to select an acting node from the request body.
  • Validate that capability on thread start, bind, observed, authorize, expose, and catalog operations.
  • Forward the node capability through the shared relay without placing the secret in relay process arguments.
  • Sign persisted thread/account/node/endpoint mappings and verify the signature before Browser Control accepts ownership.
  • Store the signing secret encrypted with Electron safeStorage; unavailable protected storage fails closed without a plaintext fallback.
  • Reject missing, invalid, cross-node, and tampered ownership claims before they reach thread handlers.
  • Allow legacy unsigned mappings to be migrated by an authenticated rebind, but never use them to authorize Browser Control.

Browser control

  • Expose only Browser nodes owned by the requesting Codex node to the bundled Codex Browser Plugin.
  • Bridge the official in-app-browser protocol to Electron webview guests through owner-scoped CDP sessions.
  • Support inspection, focus, mouse/keyboard input, click, navigation, and real scrolling.
  • Restore browser ownership after an app restart.
  • Clean debugger listeners and child CDP sessions on detach/close, including already-destroyed webContents, preventing close-time crashes and stale-session reuse.

Inter-agent mailbox and Loop automation

  • Add persistent send, reply, and status commands for existing NodeTerm agent nodes.
  • Generate timestamp, human-readable node title, stable project/node address, sender, recipient, message id, and reply instructions for every delivery.
  • Distinguish durable queued acceptance from delivery into an agent turn.
  • Deliver through the agent mailbox without creating replacement sessions or typing into terminal input.
  • Add visible, persisted Loop nodes that deliver a task to one or more agent nodes on a configured cadence.
  • Let users and agents create, update, start, pause, run immediately, inspect, and delete Loops through canvas control; deletion remains confirmation-gated.

Canvas, terminal, and file UX

  • Paste screenshots/images onto the canvas and drag image files from Finder through the existing Open File path.
  • Keep paste/drop disarmed over terminals, dialogs, settings, usage UI, editors, and other keyboard/input surfaces.
  • Copy selected local file-backed nodes to the macOS clipboard as Finder-compatible file references.
  • Route macOS two-finger trackpad gestures to canvas panning while preserving native two-finger scrolling inside terminal/native-scroll surfaces.
  • Keep an active trackpad sequence on the pan path even when Chromium emits a quantized wheel packet; ordinary mouse-wheel zoom and Cmd/Ctrl-wheel or pinch zoom remain available.
  • Reuse the Sessions-sidebar framing rules when double-clicking a node, so navigation has one padding and zoom contract.
  • Use xterm's DOM renderer for the macOS auto default to keep terminal text sharp after drag operations; explicit renderer settings remain available and Linux/Windows defaults are unchanged.

Nested groups and Sessions sidebar

  • Allow groups inside groups instead of enforcing a one-level model.
  • Make group bodies click-through for normal canvas panning; movement remains on the group header.
  • Support Shift-click additive selection and context-menu moves into an existing selected group.
  • Keep worktree/cwd inheritance correct through ancestor groups.
  • Render groups recursively in the Sessions sidebar as a tree with persisted disclosure state.
  • Keep row clicks for navigation while the disclosure chevron alone expands/collapses a group.
  • Drag sibling groups to reorder them inside their current parent while retaining session drag-to-group behavior.
  • Keep nested group order and parentage stable across workspace persistence.

Safety properties

  • no process-global NodeTerm node identity;
  • no app-server per node;
  • no hard-coded SSH host or Ubuntu-specific product path;
  • shared hook authentication cannot assign a Codex thread to an arbitrary sibling node;
  • signed ownership mappings are required before Browser Control resolves a thread to a node;
  • same-machine account switching and cross-machine copying preserve the requested conversation id;
  • explicit known-session resume cannot silently create a replacement conversation;
  • cross-machine copy retains the source and never overwrites an existing target rollout;
  • ambiguous account ownership fails closed;
  • browser routing compares authenticated thread ownership with browser ownership;
  • browser teardown never calls debugger APIs on destroyed contents;
  • managed-account storage failures cannot fall back to another login;
  • canvas paste never steals text intended for a terminal or editor.

Verification

  • npm run typecheck: passed after rebasing onto current main.
  • npm run build: passed after rebasing onto current main.
  • Current account/SSH/gesture/navigation changed-scope run: 12 files, 194 tests passed.
  • Expanded ownership/security regression run: 14 files, 177 tests passed.
  • Focused ownership/relay regression run: 6 files, 69 tests passed.
  • git diff --check: passed.
  • Local macOS ARM64 bundle built and exercised in the packaged app.

No release publishing, production deployment, Developer ID signing, notarization, or release-pipeline dispatch is part of this PR.

@proteus-dev proteus-dev changed the title Add shared Codex sessions, browser control, and nested groups feat(canvas): shared Codex sessions, browser control, and nested groups Aug 10, 2026
@eneskirca

Copy link
Copy Markdown
Owner

The hook token authenticates a session, not which node — and three open PRs now rest on it

Cross-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

hookServer mints one randomUUID() for its whole lifetime (src/core/agents/hook-server.ts:171) and buildPtyEnv injects that same value into every session it spawns (:453). Request handling checks only that token (:180), then reads the acting node straight out of the request body — parseControlBody's form.nodeId for /control/*, and form.nodeId again on the /codex-thread/* routes (:189, :220, :246).

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 nodeId only mis-attributed an edge or a cwd. These PRs change what it buys.

What it buys in each PR

#113 — per-pair trust. pairKey = [sourceNodeId, args.node].sort().join('|') makes the asserted source id one half of the grant key. Agent C, holding the same env token, can send nodeId=A and deliver into B under the A↔B grant it was never given. The dialog the grant is meant to replace never appears — that is the grant's whole purpose, so there is no second gate behind it. The global toggle is honest about being blunt ("Any control-capable agent can then type into any node's terminal unseen"); the per-pair mode reads as the precise alternative, and precision is exactly what the asserted id cannot deliver.

#112 — thread ownership, and through it, browser control. The browser router does the right thing: it demands guest.ownerNodeId === nodeIdForSession(session_id) (browser-use-backend-core.ts:281). But that mapping is writable over /codex-thread/bind, whose nodeId comes from the body. bindCodexThreadIdentity refuses only when the thread is already bound to another live node (codex-identity-proxy.ts:165-167); an unowned or dead-owner thread binds freely to whatever node id the caller names (:175). Chain: find a real unowned thread id → bind it to victim node B → connect to the browser socket claiming that session → requireTab now resolves to B, and you can inspect, click, type and navigate B's browser node, including whatever authenticated session B has open. The ownership check is sound; the thing it checks against is attacker-writable.

#98 — same ground, smaller blast radius. notify verifies the source is context-linked to the target, but the source is the asserted id. With list returning every node id on the project, an unlinked agent can iterate ids as claimed sources until one satisfies the link check. The payload is a fixed string, so this is noise-injection rather than takeover — but it is the same gate resting on the same assertion.

Scope, stated plainly

All 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 fix

Bind the token to the node, and stop reading identity from the body:

  1. buildPtyEnv mints a per-node token instead of handing out the server-wide one, and registers token → nodeId in a map alongside the existing session bookkeeping.
  2. Request handling resolves the acting node from the token (tokenMatches becomes nodeForToken), and nodeId in the body is either dropped or required to equal the resolved node.
  3. Sessions outlive the app, so the map must survive a relaunch — the endpoint file (hook-server.ts:435) already carries the restart handoff and is the natural place for the per-node entries. Unknown token ⇒ 403, which is the safe degrade.

That is one change in src/core/agents/hook-server.ts plus its buildPtyEnv callers. It closes #113's grant spoof and #112's bind spoof at once, and turns #98's link check into something that means what it says. Doing it per-PR instead would mean three partial defenses over the same hole.

Two smaller things worth folding in while that seam is open, both from #112: /codex-thread/observed lets a caller set another node's title (index.ts:1003), and the CDP passthrough in browser-use-backend-core.ts:366 forwards any method with no allowlist — so a controlling agent can Page.navigate to file:///… then Runtime.evaluate. Harmless against your own node; a file-read primitive against a victim's once the ownership above is forgeable.

What I verified, and what I did not

Read and confirmed in the code: the single token and its identical injection (:171, :453), the body-sourced nodeId on /control/* and /codex-thread/bind (:189, :220), bindCodexThreadIdentity's live-only refusal (:165-175), the browser router's ownership check (:281), and #113's pairKey construction.

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 /control/write and /control/close outright, so agent messaging only worked if the user clicked within two seconds. Neither PR created this weakness; they inherited it and lean on it harder than anything before them.

@proteus-dev

Copy link
Copy Markdown
Author

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:

  • each Codex node receives a node-bound HMAC capability;
  • thread start, bind, observed, authorize, expose, and catalog validate that capability before invoking their handlers;
  • the shared relay forwards the capability while keeping it out of process arguments;
  • persisted thread/account/node/endpoint mappings are signed, and Browser Control resolves ownership only from a valid signature;
  • the signing secret is encrypted with Electron safeStorage and the feature fails closed if protected storage is unavailable;
  • unsigned legacy mappings can be migrated by an authenticated rebind, but cannot authorize Browser Control.

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.

Corvin added 24 commits August 11, 2026 19:33
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.
@proteus-dev
proteus-dev force-pushed the codex/nodeterm-shared-codex-identity branch from ae5f03d to 3b09ff2 Compare August 11, 2026 17:36
@proteus-dev proteus-dev changed the title feat(canvas): shared Codex sessions, browser control, and nested groups feat(canvas): add shared Codex sessions, SSH accounts, browser control, and nested groups Aug 11, 2026
@proteus-dev

Copy link
Copy Markdown
Author

Updated after rebasing onto current main and resolving the terminal-renderer overlap with the upstream offscreen-disposal work.

New in the PR head:

  • machine-scoped local and SSH Codex accounts with one shared app-server per machine/account;
  • remote system-account discovery and managed device-login setup;
  • consistent account/provenance presentation across Settings, node headers, and menus;
  • atomic local-to-SSH conversation transfer that preserves the thread id and retains the local rollout;
  • remote screenshot/file upload into SSH terminals;
  • native two-finger terminal scrolling plus stable canvas panning without intermittent wheel zoom.

Post-rebase verification: typecheck passed, build passed, 12 changed-scope test files / 194 tests passed, and git diff --check passed.

pull Bot pushed a commit to jasonkneen/nodeterm that referenced this pull request Aug 13, 2026
… 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>
pull Bot pushed a commit to jasonkneen/nodeterm that referenced this pull request Aug 13, 2026
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>
eneskirca added a commit that referenced this pull request Aug 13, 2026
fix(startup): avoid keychain access at boot when no device is paired (from #112)
eneskirca added a commit that referenced this pull request Aug 13, 2026
feat(ssh): host attachment — a remote node inside a local project (extracted from #112)
dkattan pushed a commit to dkattan/nodeterm that referenced this pull request Aug 13, 2026
…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>
dkattan pushed a commit to dkattan/nodeterm that referenced this pull request Aug 13, 2026
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>
@eneskirca

Copy link
Copy Markdown
Owner

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 main (extracted and merged): the canvas utilities are all in — src/main/clipboard-files.ts, src/renderer/canvas/canvas-file-copy.ts, src/renderer/canvas/canvas-image-import.ts, and src/renderer/canvas/wheel-gesture.ts all exist on main today. Thank you for those; they were the cleanest part of the PR and they went in close to as written.

Measured and dropped: the shared-Codex-identity work (codex-accounts-core.ts, codex-identity-proxy.ts, codex-relay-daemon.ts, AccountIdentityPills.tsx). The RAM saving that motivated it was measured and does not hold — it is a net loss below roughly eight nodes, which undercuts the Eco work. Not a code-quality judgement; the premise just didn't survive measurement.

Blocked on decisions I owe you, not on the code: browser-use-backend (browser control) and the nested-groups work. Both are product-shaped questions about surface area rather than review comments I can write.

Superseded: src/renderer/state/agentMailbox.ts. Agent messaging is being redesigned right now and I do not want you rebasing onto something that is about to move.

The one piece nobody has triaged

src/renderer/lib/nativeLoop.ts + src/renderer/nodes/NativeLoopNode.tsx + workspace.native-loop.test.ts is the only slice that has neither landed nor been ruled out, and it deserves better than being buried here. The core module is ~40 lines, pure, and genuinely well thought through:

  • nextLoopRun explicitly refuses to create a catch-up burst after the machine wakes from sleep, which is the bug most schedulers ship with
  • parseLoopInterval has a tight ^([1-9][0-9]*)(m|h|d)$ grammar with Number.isSafeInteger plus range clamping, so the canvas-control cadence string cannot produce a nonsense timer
  • loopMessageId is deterministic, which is what makes at-most-once delivery possible

The catch is that its whole reason to exist is delivering a message on a schedule, so it sits directly on top of agentMailbox — the superseded piece. It should be sequenced after the messaging redesign, not before, and at that point it is a small, self-contained PR I would like to see on its own.

If you would rather not carry this branch while that happens, closing it is reasonable — the extracted slices are already on main with your authorship, and NativeLoop is small enough to re-cut fresh against the new messaging shape. Your call, and either way I owe you the browser-control and nested-groups decisions.

eneskirca added a commit that referenced this pull request Aug 15, 2026
…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>
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.

2 participants