Skip to content

Runner and Remote Sessions

Ingo Eichhorst edited this page Jul 14, 2026 · 1 revision

Runner & Remote Sessions

Status: accepted target-state design, 2026-07-14. Canonical design for issue #925. This page supersedes the future mobile, remote-control, runner, sandbox, and handover sections of Relay-Server and Platform-Rollout. docs/relay-protocol.md remains authoritative for the protocol that ships today.

1. Product outcome

Irrlicht starts as an observer: it finds AI coding sessions on a Mac and reports whether each one is working, waiting, or ready. The full remote-session solution keeps that observational core and adds an explicit execution boundary.

The result is one continuous session experience across three places:

  1. On the phone: pair once, inspect every session, answer a question, attach to the terminal when needed, and choose where new work runs.
  2. On the Mac: run an agent through irrlicht run, keep the native terminal experience, and gain reliable control because Irrlicht owns that process's PTY.
  3. On a user-owned Linux server: provision the runner once over SSH, start work in an isolated sandbox, or beam a Mac session to the server and later pull it back.

A session lives in exactly one place at a time. Clients are windows onto the authoritative runner; they are not replicas of the process.

Product invariants

  • Session state stays working | waiting | ready. Placement, connectivity, and handover are separate dimensions, not new session states.
  • Observation remains useful without control. Pairing an iPhone read-only is a complete first increment.
  • Reliable control is opt-in through irrlicht run <command>. Discovered, unwrapped sessions retain the existing injection path as a clearly labeled limited tier.
  • The native agent TUI remains the interface. The runner owns and mirrors its PTY; Irrlicht does not replace it with a protocol-specific chat client.
  • One stable Irrlicht session identity survives Mac-to-server and server-to-Mac placement changes.
  • The relay brokers identity, routing, and streams. It does not execute agents, interpret rules, or hold repository credentials.
  • Remote execution is single-user in v1 and always sandboxed. The provider is selectable and capability-driven.
  • Real credentials remain outside the sandbox. Git access uses session- and branch-scoped credentials.

2. Names and responsibilities

Name Responsibility Does not do
Irrlicht clients macOS app, web dashboard, and iOS app. Render observation, issue scoped requests, and show placement/capability state. Own an agent process or decide placement truth.
funken.io Hosted identity and pairing control plane: workspace, devices, short-lived pairing challenges, scoped credentials, revocation, and relay service discovery. The managed profile also exposes the authenticated relay endpoint. Execute user code, store repository credentials, or become the source of session state.
irrlichtrelay Broker/data plane. Fan out observations, route commands to the authoritative runner, keep placement leases, and carry attachment streams. May be managed behind funken.io or self-hosted. Run rules, inspect terminal meaning, or host sandboxes.
irrlichd Observer. Discover agent transcripts/processes, derive the three states and metrics, and publish observation events. Runs on the Mac and the Linux server. Own PTYs or migrate processes.
runner Execution boundary invoked as irrlicht run <command>. Own the PTY, mirror locally or run headless, host per-session rules, expose control/attach, and create checkpoints. Discover unrelated sessions or broker clients.
runner supervisor Long-running service on the Linux server. Advertise workspaces/providers, accept spawn requests, create runner instances, and report health/capabilities. Act as a general remote shell daemon.
sandbox provider Create/destroy an isolated environment, provide a persistent workdir and interactive PTY, enforce resource/network policy, and report capabilities. Decide product placement or hold user credentials.

irrlicht run is the public command. The implementation may ship as an irrlicht-run helper binary, but users and docs should not present two competing command names.

3. Three product epics

Epic User value Minimum dependency Acceptance demo
1. Pair and observe The iPhone shows Mac sessions through funken.io from anywhere. Existing daemon forwarder/relay observation plus device pairing. Pair by QR, leave Wi-Fi, and watch a Mac session move through all three states.
2. Control sessions on the Mac A phone can answer, send commands, interrupt, and attach to sessions launched by irrlicht run. macOS/web manage connection and devices. Stable identity, local runner, control routing, scoped device credentials. Start irrlicht run claude, answer from iPhone, attach, revoke that iPhone, and prove further control is denied.
3. Run and beam on a server A provisioned Linux box can spawn sandboxed sessions and receive a Mac session at a turn boundary. Runner supervisor, SSH setup, sandbox providers, placement/checkpoint protocol. Spawn from iPhone, then round-trip another session Mac -> server -> Mac with observation continuous.

These are product increments, not isolated engineering projects. Stable identity, capability negotiation, permission gating, and request/result semantics are shared foundations.

Three-stage iOS product progression

4. End-state topology

flowchart LR
    subgraph Clients
        IOS[iOS app]
        MAC[macOS app]
        WEB[Web app]
    end

    subgraph Hosted[funken.io hosted profile]
        CP[Identity and pairing]
        RELAY[irrlichtrelay broker]
    end

    subgraph Mac[User Mac]
        MD[irrlichd observer]
        MR[runner: irrlicht run]
        AGENT[Agent PTY]
    end

    subgraph Server[User-owned Linux server]
        SUP[Runner supervisor]
        SD[irrlichd observer]
        SP[Sandbox provider]
        SR[Headless runner]
        SAGENT[Agent PTY]
    end

    IOS -->|pair and manage device| CP
    MAC -->|pair and manage device| CP
    WEB -->|account session| CP
    IOS <-->|WSS observe/control/attach| RELAY
    MAC <-->|WSS observe/control/attach| RELAY
    WEB <-->|WSS observe/control/attach| RELAY
    MD -->|outbound observations| RELAY
    MR <-->|outbound requests/streams| RELAY
    MR --> AGENT
    SD -->|outbound observations| RELAY
    SUP <-->|outbound catalog/spawn| RELAY
    SUP --> SP --> SR --> SAGENT
Loading

No inbound port is opened on the Mac or execution server. Daemons, runners, supervisors, and clients dial out to the relay. The hosted broker is internet-reachable over TLS and rejects every operation without a scoped credential. The self-hosted profile can instead place the same relay on a Tailscale/WireGuard network.

Data paths

  1. Observe: irrlichd -> relay -> clients. Low-volume, state-convergent events; snapshot reconciliation repairs dropped/coalesced updates.
  2. Control: client -> relay -> authoritative runner. Ordered requests with acceptance and completion/error results.
  3. Attach: authoritative runner -> relay -> attached clients. Initial VT snapshot followed by ordered deltas; input still uses the control path.
  4. Placement: runner/supervisor <-> relay -> clients. A generation-checked lease says which runner is authoritative.
  5. Checkpoint: source runner -> target runner, brokered by relay/object transfer. Contains work and native session data, never a live process image.

5. Session identity, placement, and ownership

Today relay identity is (workspace, daemon_id, session_id). That is sufficient for observation but cannot survive handover because daemon_id changes when a session moves.

The remote-session model introduces four distinct identifiers:

Identifier Lifetime Purpose
session_key Entire Irrlicht session Random, globally stable identity used by clients, rules, parent links, and placement.
agent_session_id Agent-native conversation Claude/Codex/etc. resume identity. May need path remapping when moved.
runtime_id One process incarnation Identifies the current PTY/process. Changes after resume or handover.
runner_id One installed runner/supervisor Routing and capability identity for a Mac or server.

Target placement shape:

{
  "session_key": "ses_01K...",
  "generation": 7,
  "runner_id": "run_buildbox",
  "runner_label": "buildbox",
  "location": "server",
  "runtime_id": "rt_01K...",
  "observer_daemon_id": "daemon_server",
  "workspace_id": "ws_personal",
  "sandbox": {
    "provider": "srt",
    "sandbox_id": "sbx_01K...",
    "network": "allowlist"
  },
  "capabilities": ["control", "interrupt", "attach", "checkpoint", "resumable"]
}

Authority rules

  • The relay routes by session_key and the current placement lease, never by a client-supplied daemon target.
  • Every placement mutation compares generation; stale requests fail instead of creating two authorities.
  • The source retains authority until the target runner has restored the checkpoint and confirmed readiness. The relay then atomically advances the generation.
  • A lease expires when a runner disappears, but the relay does not silently start a duplicate. The session becomes unavailable until the runner reconnects or the user chooses an explicit recovery.
  • Host-local PID, CWD, transcript path, launcher, and observer daemon stay runtime metadata. They never define global identity.

Runner/observer registration

Before spawning an agent, the runner registers session_key + runtime_id + owned_pid with the local daemon. The daemon suppresses process-presession discovery for that owned PID, then binds the eventual transcript-native session to the registered key. This handshake is complete before the child can be reported, eliminating the current duplicate-discovery race.

Observation still comes from irrlichd; ownership and control come from the runner. A temporary observer outage must not remove runner authority, and a runner outage must not rewrite the last observed state.

6. Trust and credential model

Deployment profiles

Profile Pairing/control plane Broker exposure Intended use
Managed funken.io funken.io Public wss:// behind TLS, auth, workspace isolation, limits, and revocation Phone access without operating a broker.
Self-hosted tailnet Local CLI/admin UI or optional funken pairing Private Tailscale/WireGuard address Maximum infrastructure control.
Trusted LAN development Manual token Loopback/LAN only Development and protocol testing, not remote phone access.

The managed profile intentionally supersedes issue #925's earlier absolute statement that the relay is never internet-facing. The invariant that remains is: no user machine exposes an inbound port, and no unauthenticated relay operation exists.

Pairing flow

sequenceDiagram
    autonumber
    participant M as macOS Remote Access
    participant F as funken.io control plane
    participant I as iOS app
    participant R as relay

    M->>F: Create one-time pairing challenge
    F-->>M: QR payload + short code + 5-minute expiry
    I->>F: Redeem challenge, present device public key
    F-->>M: Confirm device name and requested observe scope
    M->>F: Approve
    F-->>I: Device credential bound to key, workspace, role, scopes
    I->>R: hello(device credential, proof of key)
    R-->>I: snapshot limited by granted scopes
Loading

The QR code never contains the workspace's long-lived bearer token. It contains a one-time opaque challenge, the control-plane origin, and a pinned workspace identifier. Pair approval shows device name, platform, requested scopes, and expiry.

Credential classes

Credential Storage Scope and lifetime
Device credential iOS/macOS Keychain, bound to a device key One workspace; observe, optional control, attach, spawn, handover; revocable; rotating.
Browser session HttpOnly, Secure, SameSite cookie for hosted web; local daemon proxy for local web Short-lived account session. Long-lived relay tokens do not enter localStorage.
Runner credential Mode 0600 file or OS key store on runner host Runner role, one workspace, catalog/spawn/control for its sessions only.
Daemon credential Existing protected relay token store, upgraded with daemon role Publish observations for its daemon identity only.
Session grant In-memory, signed/opaque capability One session and operation set; short expiry; used for attach/handover.
Git credential Host-side credential helper One repository/session branch, short expiry; never copied into sandbox files.

Permission gating

Every read or modification performed by a new adapter remains consent-gated:

  • PTY input, interrupt, attach output, and checkpoint reads are runner permissions.
  • SSH probing/install, service changes, and remote file writes are provisioning permissions.
  • Sandbox creation, workdir mounts, network policy, and exposed ports are provider permissions.
  • Git credential minting is a separate modify permission with branch/repository scope.
  • Relay toggles remain an outer gate; per-device scope, per-agent consent, session capability, and placement authority are all checked again at the executing runner.

The runner/provider implementations require behavioral coverage equivalent to contracttesting.AssertPermissionGated; transport authorization never replaces local consent.

Relay confidentiality

V1 uses TLS and a trusted broker. Framing keeps routing metadata separate from payloads so end-to-end encryption can be added later. E2E changes notification design: APNs may need generic β€œA session needs you” payloads unless a user device encrypts notification content for itself.

7. Epic 1 - pair and observe from iOS

Epic 1 is intentionally read-only. It validates device identity, WAN reconnect, aggregated session identity, and mobile information hierarchy before remote commands raise the security stakes.

User flow

  1. In macOS Remote Access, the user signs into or selects funken.io and sees the connection health.
  2. Pair device creates an expiring QR code and short code.
  3. The iOS app scans the code, names itself, requests observe, and waits for approval.
  4. The Mac shows the request; the user approves.
  5. The phone receives an initial workspace/daemon/session snapshot, then live updates.
  6. Session rows say where work is running: On this Mac or Server - buildbox. A cloud icon alone is not sufficient.
  7. Tapping a row opens state, question/summary, project/branch, model/context, cost, source, last update, and connection quality.

iOS application structure

There is no iOS target today. The implementation starts by extracting transport-neutral Swift code from the macOS app:

IrrlichtCore
  Models/               session, placement, capabilities, questions
  Transport/            daemon + relay streams, auth, reconnect, snapshots
  Identity/             pairing, Keychain credentials, device key
  Notifications/        transition policy, presence, deep links
  Presentation/         grouping, ordering, source/placement labels

IrrlichtMac             menu-bar shell, launch/focus, macOS notifications
IrrlichtMobile          SwiftUI navigation, QR scanner, APNs, terminal view

The first mobile target is native SwiftUI with NavigationStack on iPhone and adaptive master/detail on iPad. It consumes the same canonical models and relay envelope as macOS. Platform actions such as local terminal activation remain outside IrrlichtCore.

Observation behavior

  • Initial connection is always hello -> capability agreement -> snapshot -> live.
  • The phone keys rows by session_key, not daemon/session tuples.
  • Snapshot reconciliation restores truth after sleep, radio changes, or dropped coalescible events.
  • A daemon or runner disconnect changes connectivity/availability badges without inventing a fourth session state.
  • The last observation remains visible with Offline - last seen ... instead of disappearing.
  • Local and relay paths may report the same session; stable identity collapses them deterministically.
  • Parent/child relationships and grouped project views remain intact across sources.

Notifications

  • Default triggers are working -> waiting and an optional long-tail ready notification.
  • The app registers its APNs token against its device identity, not a raw workspace token.
  • Tapping a notification deep-links to the session detail. Epic 1 has no reply action.
  • When a paired Mac later publishes that the same session's terminal is focused, phone notifications for that session are suppressed. This presence signal arrives in Epic 2; until then iOS uses local foreground suppression only.
  • A disconnected relay or expired credential produces a visible connection state and one actionable notification, not repeated session alerts.

Failure and degraded behavior

Condition Behavior
Mac sleeps or loses network Keep last snapshot, mark Mac/source offline, reconcile on reconnect.
Relay unavailable Exponential backoff with jitter; no local data is deleted.
Device revoked Close with a distinct auth code, erase credential, return to pairing.
Protocol capability missing Render supported observation fields and hide unavailable actions.
APNs unavailable Live data still works while the app is open; show notification status in settings.

Definition of done

  • A new iPhone pairs using a one-time QR challenge; the QR does not reveal a reusable token.
  • The device can be approved, listed, and revoked from the Mac.
  • On cellular data, the iPhone displays all Mac sessions with correct state, source, placement, parent/child grouping, and offline behavior.
  • All three state transitions reconcile after an intentional disconnect/reconnect.
  • The paired credential grants observation only; control frames from that device are rejected.
  • iOS model/transport behavior is shared with macOS rather than duplicated.

8. Epic 2 - reliable control of sessions on the Mac

Epic 2 introduces the runner locally, where PTY behavior, terminal mirroring, identity registration, and rule execution can be debugged without server provisioning.

Launch contract

irrlicht run claude
irrlicht run codex -- <agent arguments>
irrlicht run --sandbox null -- gemini

The runner:

  1. Allocates session_key and runtime_id.
  2. Registers ownership with the local daemon before starting the child.
  3. Creates a PTY through the selected local provider.
  4. Starts the command with the user's terminal size and environment policy.
  5. Mirrors bytes between the PTY and local terminal.
  6. Runs an always-on VT state machine for instant attach and UI prompt detection.
  7. Hosts the effective backchannel rule snapshot for that session.
  8. Connects outbound to the relay and advertises control/attach capabilities.
  9. On exit, flushes final observation/checkpoint metadata and releases placement.

The wrapper is explicit. An optional shell integration may shadow common agent commands only where native tmux/kitty/iTerm/Terminal control is unavailable, but it must be managed, idempotent, visible, and removable.

Interaction tiers

Tier Wrapped session Unwrapped discovered session
Observe Transcript/process observation via daemon Same
Control Reliable PTY write/signal through authoritative runner Best-effort injection through tmux/kitty/AppleScript when available
Attach VT snapshot + ordered deltas Unavailable unless an adapter can provide an equivalent owned stream
Handover Available when agent is resumable Unavailable; offer restart through irrlicht run

Clients label these as Reliable, Limited, or Observe only. They do not present injection as equivalent to runner ownership.

Control request semantics

Control is no longer a silent fire-and-forget frame:

{
  "type": "control_request",
  "request_id": "req_01K...",
  "session_key": "ses_01K...",
  "placement_generation": 3,
  "action": "input",
  "text": "/compact\r",
  "client_seq": 42
}

The broker returns request_accepted after authorization and routing. The runner returns request_result with applied | rejected | failed, its authoritative generation, and a user-safe error code. Input is ordered per device/session. A client may retry the same request_id; the runner deduplicates it. Interrupt is a distinct action and never inferred from text.

Checks occur in this order:

  1. Device credential permits the action.
  2. Workspace/session grant includes the session.
  3. Relay routes to the current placement lease.
  4. Runner relay-control toggle is enabled.
  5. Agent/provider permission is granted locally.
  6. Session advertises the capability.
  7. Request generation and ordering are current.

Attach stream

Attach is downstream terminal output plus the existing ordered input path:

sequenceDiagram
    autonumber
    participant C as Client
    participant B as Relay
    participant R as Runner

    C->>B: attach_request(session_key, cols, rows)
    B->>R: attach_request(attachment_id, grant)
    R-->>C: vt_snapshot(seq=900, screen, cursor, modes)
    loop While attached
        R-->>C: vt_delta(seq=901..n, bytes)
        C->>B: attach_credit(last_applied, bytes_available)
    end
    alt client falls behind or delta is lost
        R-->>C: resync_required
        R-->>C: vt_snapshot(new seq)
    end
    C->>B: detach(attachment_id)
Loading

PTY traffic never uses the relay's ordinary drop-when-full observation queue. Each attachment has its own ordered sequence, credit window, idle timeout, and maximum buffered bytes. Overflow causes snapshot resynchronization. Terminal resize is an explicit control request; multiple attached clients can view, but one device holds the input lease by default.

Rules move with the runner

The current global backchannel rules are resolved into an effective per-session rule snapshot at launch. The runner subscribes to observed state and VT UI signals, applies cooldown/global limits, and writes through its PTY controller. A checkpoint contains the rule snapshot and whether cooldown timestamps should continue. Editing global rules does not silently rewrite a running remote session; the UI offers Apply updated rules.

macOS and web control surfaces

  • A session row exposes one keyboard/control icon only when at least one control capability is available.
  • The detail action sheet contains the observed question, command input, Send, Interrupt, and Attach where supported.
  • The current macOS control popover becomes transport-neutral; it routes by placement rather than hardcoded loopback HTTP.
  • Web gains the same capability-aware action model. Terminal attach uses xterm.js or an equivalent proven terminal renderer; it does not implement ANSI/VT parsing itself.
  • Remote notification taps open session detail/attach rather than trying to activate a terminal on another host.

Remote Access configuration

Remote connectivity is promoted out of Advanced Settings into a dedicated macOS full-panel destination and a dedicated web dialog/page. Both use the same information architecture:

  1. Connection - funken workspace/profile, broker URL, status, last sync, and self-hosted override.
  2. Pair a device - expiring QR/short code, requested default scopes, cancel/regenerate.
  3. Devices - name, platform, scopes, last seen, current presence, edit scope, revoke.
  4. This Mac - publish observation, allow remote control, default runner/provider, focus presence.
  5. Runners - configured Linux servers, health/version, advertised providers, setup/reconfigure/remove.
  6. Security - credential rotation, audit events, and destructive disconnect workspace action.

Raw bearer tokens are not the primary UI. Manual tokens and URLs remain under an advanced self-hosted section.

Remote Access configuration on macOS and web

Presence and notification suppression

The Mac runner publishes short-lived presence for session_key when its terminal has focus. The relay keeps only the latest expiring presence and clients use it to suppress redundant phone notifications. Presence never changes authority and is not the existing host-local focus_requested activation frame.

Definition of done

  • A Claude Code session launched with irrlicht run appears once, with stable identity and Reliable control.
  • iOS, macOS, and web can answer, send a command including submission CR, interrupt, and receive explicit success/failure.
  • A late attach receives a correct screen immediately, survives resize, and resynchronizes after forced overflow.
  • An unwrapped controllable terminal still works and is labeled Limited; an unsupported editor is Observe only with a wrapper offer.
  • Pairing, scope changes, device list, last-seen, and revoke work from macOS and web Remote Access.
  • Revocation terminates the device connection and blocks a replayed request.
  • Runner, device, and attach modify paths pass permission-gating contract tests.

9. Epic 3 - provision a server, spawn, and beam

The execution server is a user-owned Linux machine. It runs a supervisor, irrlichd, and one runner per active session. funken.io brokers access but never becomes the execution environment.

Setup prerequisites

  • Linux on amd64 or arm64, with systemd available for the initial implementation.
  • Outbound HTTPS/WSS and package/container registry access according to chosen provider policy.
  • SSH access using an existing SSH agent/key. Password auth may be entered interactively but is never stored by Irrlicht.
  • A non-root user with a writable home directory. Root is not required for the base install.
  • Optional: Docker/Podman and permission to use it; bubblewrap/SRT dependencies; Git and an agent CLI.

SSH provisioning flow

sequenceDiagram
    autonumber
    participant U as macOS/web setup UI
    participant P as Provisioning service
    participant S as Linux host over SSH
    participant F as funken.io/relay

    U->>P: Add server user@host
    P->>S: Connect and obtain host key
    P-->>U: Show fingerprint for explicit trust
    U->>P: Approve host key
    P->>S: Probe OS/arch/systemd/providers
    P-->>U: Show install plan and permission effects
    U->>P: Approve
    P->>S: Install signed versioned binaries and config
    P->>S: Install/enable user systemd units
    P->>F: Mint runner + daemon credentials
    P->>S: Store scoped credentials mode 0600
    S->>F: Outbound supervisor and daemon hello
    F-->>U: Runner online + catalog/capabilities
Loading

Provisioning is host-key-verified, idempotent, and resumable. It records the accepted fingerprint and refuses a changed key until the user explicitly re-trusts it. Every remote modification is previewed as an effect and audited.

The base install uses user-owned paths:

~/.local/bin/irrlicht
~/.local/bin/irrlichd
~/.config/irrlicht/server.toml
~/.local/share/irrlicht/
~/.config/systemd/user/irrlicht-runner.service
~/.config/systemd/user/irrlichd.service

The installer enables user lingering only with explicit consent. Provider-specific setup that needs elevated rights is a separate step with a separate permission; the base install does not run a remote shell script as root.

Server runtime

  • The supervisor advertises label, version, health, capacity, agents, workspaces, provider capabilities, and supported resume adapters.
  • irrlichd observes server sessions and publishes the same session/state events as the Mac daemon.
  • A spawn request is validated, creates a sandbox, starts a headless runner within it, and returns a stable session_key before the child agent starts.
  • Runner and daemon use the same registration handshake as on the Mac, so server sessions appear once.
  • The supervisor survives relay outages, keeps local runners alive, and reconciles catalog/placement when it reconnects.
  • Updating the server drains or preserves existing runners; it does not terminate them without an explicit incompatible-upgrade warning.

SandboxProvider contract

type SandboxProvider interface {
    Capabilities(ctx context.Context) CapabilitySet
    Create(ctx context.Context, spec SandboxSpec) (Sandbox, error)
    Destroy(ctx context.Context, id SandboxID) error
    ExecPTY(ctx context.Context, id SandboxID, spec ExecSpec, size Size) (PTY, error)
}

type PTY interface {
    Read([]byte) (int, error)
    Write([]byte) (int, error)
    Resize(Size) error
    Signal(Signal) error
    Wait() (ExitStatus, error)
    Close() error
}

SandboxSpec includes workdir source, persistent-workdir name, resource limits, environment allowlist, mounts, secret proxies, and network policy:

none
allowlist(domains)
full

Providers advertise persistent_workdir, network_none, network_allowlist, network_full, snapshot_restore, expose_port, devcontainer, and resource-limit capabilities. The UI disables unsupported choices and explains the degradation before spawn.

Provider sequence

Provider Role Remote default?
SRT/bubblewrap Lightweight host-toolchain sandbox with filesystem boundaries and allowlisted egress where supported. Default for a basic Linux server after capability probe.
Docker/Podman + devcontainer Reproducible OCI environment, persistent volumes, devcontainer configuration, stronger dependency isolation. Recommended for project-defined environments.
null Plain directory/worktree. Validates the port and supports local migration debugging. Never the remote default; requires an explicit No isolation choice.
Later providers Kata/Docker sandboxed containers, Kubernetes Agent Sandbox, E2B, Daytona, Modal, Fly Machines. Behind the same capabilities, not new client protocols.

β€œEverything runs in a sandbox” therefore means remote spawn selects an isolating provider by default. null is an explicit development escape hatch, not a silent fallback. If no isolating provider is available, setup finishes with Runner online - no safe provider and blocks remote spawn until the user installs one or opts into no isolation.

Spawn from phone or web

The supervisor advertises a catalog of workspaces, agents, and providers. Clients support two paths:

  1. Catalog spawn: choose workspace/repository, agent, provider, network preset, initial prompt, and resource size.
  2. Empty sandbox: create a persistent workdir with a plain shell, attach, then run gh auth login, clone a repository, perform an agent device login, and launch the agent. Authentication URLs open in the phone browser.

The host-side credential helper answers Git credential requests with session-scoped credentials. It restricts push to the session branch and keeps real tokens outside the sandbox. Agent OAuth is performed interactively per sandbox/provider or through an explicit provider secret proxy; handover never copies arbitrary host secrets.

Beam Mac -> server

Handover is checkpoint/resume at a turn boundary, not process migration:

sequenceDiagram
    autonumber
    participant M as Mac runner
    participant B as Relay/placement
    participant S as Server supervisor/runner
    participant C as Clients

    M->>M: Drain to waiting/ready or interrupt
    M->>M: Commit work to session branch
    M->>M: Build checkpoint bundle
    M->>S: Transfer branch reference + native session files + rules
    S->>S: Create sandbox, checkout, remap paths, resume same agent
    S-->>B: Target ready(session_key, expected generation)
    B->>B: Advance placement generation atomically
    B-->>C: placement_updated(Mac -> server)
    B-->>M: Release old runtime
Loading

Checkpoint bundle:

manifest.json
  session_key, source generation, agent adapter/version
  agent_session_id, source/target path map
  git remote, session branch, commit, dirty/untracked policy
  effective rule snapshot and cooldown policy
  provider requirements and capability constraints
  warnings: background processes, MCP state, ports, secrets

agent-session/     minimum native files required for same-agent resume
transcript/        observation continuity/import material
workspace.patch    fallback for data not safely committed

The source process is not terminated until the target proves it resumed and placement flips. Failure before the flip leaves the Mac authoritative. Failure after the flip uses an explicit recovery action; automatic split-brain recovery is forbidden.

Pull server -> Mac

irrlicht pull <session> supports:

Landing mode Preconditions Result
Same project directory Same normalized Git remote; clean tree or user-approved commit/stash; compatible agent installed. Fetch/checkout session branch, import native session, resume in current directory.
New worktree Local checkout of same repository. Create a worktree on the session branch and resume without disturbing the current checkout.
Fresh directory No local checkout. Clone origin, or clone from the server runner when the sandbox-born repository has no remote; import and resume.

Only same-agent native resume is in scope. Cross-agent handoff through a distilled prompt is lossy and remains a separate future feature.

Failure and recovery

Failure Required behavior
SSH provisioning interrupted Re-run probes and converge idempotently; never assume partial success.
Server relay outage Keep local runners alive, buffer only bounded control results, reconcile placements/catalog on reconnect.
Sandbox creation fails Return structured provider/capability error; no placement is created.
Resume adapter incompatible Keep source authoritative and show the exact incompatibility.
Target starts but placement CAS fails Terminate/quarantine target runtime; source remains authoritative.
Source disappears mid-handover Do not promote incomplete target automatically; offer recovery from last complete checkpoint.
Sandbox-born repo has no remote Runner serves a read-only/push-scoped Git endpoint over the private authenticated path for fresh-dir pull.
Background shell/MCP/port cannot resume Detect before checkpoint, list losses, require confirmation.

Definition of done

  • A Linux server is configured from Remote Access using SSH, explicit host-key trust, and an idempotent install plan.
  • Supervisor and server daemon connect outbound, appear healthy, and advertise at least one isolating provider.
  • From iPhone/web, a user can create an empty sandbox, attach, authenticate via device flow, clone, and launch an agent.
  • The session remains observable and controllable after detaching the terminal.
  • A wrapped Mac session beams at a turn boundary, retains session_key, and continues observation on the server without duplicate rows.
  • The same session pulls back into same-dir and worktree modes; a phone-born/no-remote project lands through fresh-dir runner-served Git.
  • Failure injection proves source-authoritative rollback before placement flip and prevents split brain after a stale generation.

10. Screen progression

The visual artifact is a target-state storyboard, not a screenshot of shipping software. It uses Irrlicht's existing semantic colors and compact operational language.

Stage 1 - pair and observe

  • Pairing is the entry screen only until a credential exists.
  • The session list leads with state, project/session, observed question, and explicit placement.
  • On this Mac and Server - buildbox are readable labels; a source glyph is secondary.
  • Offline sources remain visible and show last-seen age.
  • No command composer appears for an observe-only device.

Stage 2 - control and attach

  • A waiting-session detail leads with the agent's question and context needed to answer it.
  • Suggested answers are conveniences, never automatic approval.
  • The command composer is per session and labels its delivery tier: Reliable or Limited.
  • Interrupt is destructive/secondary and asks for confirmation if a tool is active.
  • Attach opens a dedicated terminal view with connection/resize/input-lease status.

Stage 3 - spawn and beam

  • New session chooses target, workspace, agent, provider, network policy, and optional prompt.
  • Beam to server is a session action available only at a safe boundary and for resumable agents.
  • Progress names real phases: draining, checkpointing, transferring, restoring, placement switched.
  • Warnings about dirty work, background processes, MCP state, provider capability, or credentials appear before the action, not after failure.
  • Successful placement is visible in every client immediately.

11. Remote Access information architecture

Remote Access is an operational settings surface, not a setup wizard that disappears. The same destination handles initial connection and later device/server administration.

macOS

  • Open as a full 380-point panel view from Settings or a dedicated header action; do not add a fifth cramped footer button.
  • Use native SwiftUI controls, SF Symbols, Keychain, confirmation sheets, and the existing compact 4/8/12/16 spacing.
  • Pairing expands inline so the QR, expiry, requested scope, and waiting approval are visible together.
  • Device and runner rows are dense lists with status dots and an overflow menu; they are not nested cards.

Web

  • Use a dedicated Remote Access dialog/page wider than the current 420-pixel generic settings dialog when device and runner tables are present.
  • Local-daemon web delegates credential storage and SSH actions to the daemon; it never receives private keys or long-lived relay credentials.
  • Hosted web uses account session cookies and WebAuthn/device confirmation where available.
  • Every state/action must remain usable at a mobile browser width, but native iOS remains the primary phone client.

Shared control states

Area States
Connection disconnected, connecting, connected, reconnecting, credential expired
Pairing idle, challenge active, device requesting, approved, expired/cancelled
Device online, recently seen, offline, scope changed, revoked
Runner setup required, provisioning, online, degraded, incompatible version, offline
Provider available, missing dependency, capability-limited, disabled by policy

12. Protocol evolution

The shipped protocol stays at its current version until implementation. New peers negotiate capabilities in hello; required incompatible envelope changes bump the protocol version.

Roles

Role Existing/new Allowed direction
client Existing, extended Observe; scoped control/attach/spawn/handover requests.
daemon Existing, narrowed Publish observation snapshots/events only.
runner New Own/control one or more local sessions; publish placement/capabilities/results/PTY output.
supervisor New Advertise catalog/capacity and accept spawn/provisioned runner lifecycle requests.

Credentials are role-bound. A client token cannot announce itself as a daemon or runner.

Frame families

Family Frames Notes
Identity hello, hello_ack, device_presence Capability strings, role enforcement, device/runner identity.
Observation daemon_snapshot, snapshot, push, daemon_status Existing state-convergent path, extended with stable session/placement references.
Placement placement_snapshot, placement_updated, placement_unavailable Generation/lease and authoritative runner.
Catalog runner_catalog, runner_status, capacity_updated Agents, workspaces, providers, resume adapters, health.
Control control_request, request_accepted, request_result Request ID, generation, ordered input, explicit errors.
Attach attach_request, attach_result, vt_snapshot, vt_delta, attach_credit, resync_required, detach Dedicated queue and flow control.
Spawn spawn_request, spawn_progress, spawn_result, terminate_request Catalog or empty-sandbox spawn; explicit lifecycle.
Handover checkpoint_request, checkpoint_ready, handover_request, handover_progress, handover_result Turn-boundary checkpoint, capability/precondition report, CAS placement.
Device/admin Control-plane API, not general relay fan-out Pair, approve, scope, list, revoke, rotate; audit events may be streamed to authorized clients.

Request envelope

All mutating operations use the same contract:

{
  "type": "spawn_request",
  "request_id": "req_01K...",
  "idempotency_key": "device-uuid:173",
  "workspace_id": "ws_personal",
  "device_id": "dev_iphone",
  "issued_at": "2026-07-14T20:30:00Z",
  "expires_at": "2026-07-14T20:31:00Z",
  "payload": {}
}
  • request_accepted means authorized and durably assigned to an authoritative runner/supervisor; it does not mean completed.
  • request_result is terminal and replayable for an idempotency window.
  • Errors use stable codes plus safe display text; internal paths/credentials never cross the relay.
  • Expired requests are rejected even if a disconnected client later retries them.
  • Per-session control ordering is monotonic. Spawn/handover ordering is per target runner and bounded by capacity.

Placement persistence

The current relay is single-node and in-memory. Handover requires placement generation and in-flight request results to survive a broker restart. Before Epic 3, the broker adds a small durable store or reconstructs authoritative placement through a strict runner reconciliation protocol. Clients never infer authority from the most recent observation.

For a single-node v1, an embedded durable store is sufficient. Horizontal scale and general event history remain out of scope.

13. Engineering dependency map

The customer-visible sequence and implementation sequence overlap but are not identical:

flowchart TD
    F1[Stable session identity and placement model]
    F2[Hostable control package and permission subjects]
    F3[Scoped device credentials and role-bound relay auth]
    E1[Epic 1: iOS observe]
    R1[Local owned-PTY runner + daemon registration]
    C1[Routed request/result control]
    A1[VT snapshot/delta attach]
    E2[Epic 2: remote control]
    P1[SSH provisioner + supervisor]
    S1[SandboxProvider: SRT, Docker, null]
    H1[Checkpoint/resume + placement CAS]
    E3[Epic 3: server spawn and beam]

    F1 --> E1
    F3 --> E1
    F1 --> R1
    F2 --> R1
    R1 --> C1 --> E2
    R1 --> A1 --> E2
    F3 --> E2
    E1 --> E2
    P1 --> E3
    S1 --> E3
    R1 --> H1 --> E3
    E2 --> E3
Loading

Suggested implementation slices:

  1. Extract stable identity/placement projections without changing existing observation behavior.
  2. Add role-bound credentials, one-time pairing, and device administration.
  3. Extract a shared Swift transport/model core and ship read-only iOS observation.
  4. Extract the control/rules engine into a hostable package.
  5. Ship the local runner with null provider for development, registration handshake, local mirroring, and reliable control.
  6. Add VT state, attach flow control, and transport-neutral macOS/web controls.
  7. Add SSH provisioner, Linux supervisor, SRT and Docker providers, and catalog/empty-sandbox spawn.
  8. Prove checkpoint -> kill -> resume on one machine, then add generation-checked beam/pull.
  9. Add focus presence, notification suppression, credential hardening, and recovery/audit polish.

Each slice must preserve the hexagonal direction domain -> ports -> adapters -> application/services: provider and provisioning interfaces are ports; concrete SSH, Docker, SRT, VT, and relay implementations are adapters. The runner may host application services but must not import cmd/irrlichd or concrete inbound adapters.

14. Acceptance journey for the full solution

One end-to-end release candidate must pass this exact journey:

  1. Start from an unpaired iPhone, an installed Mac app, and a fresh supported Linux host.
  2. Connect the Mac to funken.io; pair the iPhone with read-only scope.
  3. Launch an unwrapped agent and confirm iPhone observation works but control is labeled limited/unavailable.
  4. Launch irrlicht run claude; confirm it appears once and is labeled On this Mac - Reliable.
  5. Answer a waiting question from the iPhone and verify explicit success plus terminal output.
  6. Attach from web late in the session and verify the first frame is a correct screen snapshot.
  7. Revoke the iPhone; prove its open socket closes and replayed input fails.
  8. Re-pair with control/spawn scope.
  9. Add the Linux server over SSH, verify its host key, review effects, and complete idempotent setup.
  10. Spawn an empty SRT sandbox from iPhone, attach, complete a device-code login, clone, and launch an agent.
  11. Detach; verify observation/control continue with no terminal bandwidth stream.
  12. Beam the Mac runner session to the server at a turn boundary; verify one stable row and placement generation advance.
  13. Simulate relay reconnect and confirm snapshots/placement recover without duplicate authority.
  14. Pull the phone-born session into a fresh Mac directory and the handed-over session into a worktree.
  15. Confirm branch/transcript continuity, rule continuity, explicit warnings for non-resumable state, and no real credential inside either sandbox.

15. Non-goals

  • Live process/VM migration across macOS and Linux.
  • Two active writable copies of one session.
  • Cross-agent handover such as Claude -> Codex.
  • Multi-user collaborative terminal input or shared session ownership.
  • Multi-tenant execution isolation guarantees in the user-owned server.
  • Building a custom ANSI/VT engine when a proven library is suitable.
  • General SSH terminal management through Irrlicht.
  • Kubernetes, microVM, or hosted sandbox implementation in the first server epic.
  • Long-term relay event history, general audit analytics, or horizontal relay scale.
  • Automatic transfer of background shells, arbitrary MCP server memory, ports, or host secrets.

16. Risks and decisions still to prove

Risk Required proof/decision
PTY flow control corrupts late/slow clients Prototype snapshot/delta/credit/resync under loss before selecting frame sizes and buffers.
Runner/daemon double discovery Race tests where transcript/process discovery occurs before, during, and after registration.
Stable identity migration breaks existing clients Introduce additive fields and deterministic fallback; test mixed old/new peers.
Hosted broker sees plaintext Threat-model TLS broker v1; decide E2E key exchange before claiming untrusted-relay security.
APNs conflicts with E2E Choose generic encrypted-safe notifications or a user-device notification producer.
Browser credential safety Keep long-lived credentials out of localStorage; define daemon proxy and hosted cookie/WebAuthn flows.
Placement durability Select embedded durable store vs runner-led reconstruction before handover implementation.
SRT/bubblewrap support varies by distro Capability probe and block safe spawn when isolation is unavailable; never silently fall back to null.
Resume files use absolute paths Per-agent adapters must remap and test native session stores across directories/machines.
Stateful MCP/background processes are lost Detect, list, warn, and require confirmation; do not claim transparent migration.
Sandbox-born repo lacks a remote Deliver runner-served Git in the first pull implementation, not as later polish.
Scoped Git credentials are provider-specific Define host-side helper/proxy semantics before exposing private repo spawn.

17. Prior art and references

The architecture borrows proven pieces, but the local-to-server push direction remains the differentiator: work and native session state move together, authority flips once, and the original process is never mirrored.

Clone this wiki locally