Skip to content

design: laptop-agent — resident execution agent on the laptop (replace per-command SSH spawns) #5343

Closed

Description

@dezren39

Design: laptop-agent — resident execution agent on the laptop (fleet infra)

Directed by Drew 2026-11-04. Full design: files/laptop-agent-design-2026-11-04.md in the repo-health goal workspace (Muse-side); substance summarized here.

Problem

Sandbox load ~11 on 4 cores. The SSH CONNECT relay measures 0% CPU — the cost is process-spawn volume (a fresh bash→ssh→helper→remote-shell tree per fleet action, ×26 code lanes, dozens of calls per shift) and session fragility (long laptop test runs strand when a session/claim lease ends mid-run — Lane H, 2026-11-04, task 9108; files move by tar-pipe because heredocs through the SSH helper mangle quoting). A Rust rewrite of the CONNECT relay was evaluated and rejected: it is a byte pipe after a ~30 ms handshake.

Verified fact the design rests on

The sandbox reaches the laptop via the VM's tailnet tunnel proxy (HTTP CONNECT). On 2026-11-04 it was proven live that the proxy CONNECTs to arbitrary ports on the laptop's tailnet IP (100.99.113.103), not just :22 — a throwaway HTTP server on the laptop served a full response to a raw CONNECT client on the sandbox. So a laptop daemon is reachable with no SSH in the data path.

Proposal

  • Daemon (laptop, host amd): single-file Python-stdlib HTTP service, systemd user unit, binds only 100.99.113.103:4799, token auth (X-Agent-Token, mode-600 token files both ends, never in argv/git). API:
    • GET /health
    • POST /exec — a batch of commands in one request, per-command exit/stdout/stderr/duration, bounded timeout + output caps
    • POST /jobs / GET /jobs/<id> (byte-offset polling) / POST /jobs/<id>/kill / GET /jobs — detached jobs that survive client/SSH death; output spooled to disk, reaped after 24 h
  • Client (sandbox): ~/tools/laptop-agent.py — performs the CONNECT handshake (credential from HTTPS_PROXY, inside the request only), subcommands health|exec|run|poll|kill|jobs.
  • laptop-ssh.sh stays for interactive/sudo use; the daemon is additive. Fleet rules unchanged (no sandbox worktrees; builds on laptop/CI).

Phases

  1. MVP — daemon + client + user service; verified end-to-end vs the ssh baseline. (Built 2026-11-04 alongside this filing.)
  2. Migrate hot call sites — creator/lane laptop probes and test runs move to run/poll; measure lane shift wall-time before/after.
  3. Durable form — declare the service in the laptop's NixOS config (dezren39/nix) so a rebuild can't drop it.

Risks

Daemon = RCE for token/proxy-credential holders; mitigations are the tailnet-only bind, token auth, and secrets kept out of argv and git. A wedged daemon blocks migrated call sites; ssh remains the fallback and the unit restarts on failure.

Acceptance criteria

  • MVP daemon + client are verified end-to-end against the SSH baseline (health, exec batch, detached jobs with byte-offset polling).
  • The daemon binds only the laptop tailnet address, requires token auth, and the token never appears in argv, git, or logs.
  • Phase 2 migration of hot call sites (creator/lane laptop probes and test runs) is done with lane shift wall-time measured before and after.
  • Phase 3 durable form is declared in the laptop's NixOS config (dezren39/nix), or an exception is recorded on this issue.
  • Each phase is tracked by a linked implementation issue or native sub-issue — a design with no implementation link is not finished work.

Activity

  1. added
    kind/performancePerformance problem or optimization
    kind/infrastructureInfrastructure / host-portability work
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    on Nov 4, 2026
  2. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    Phase 1 (MVP) built and verified — 2026-11-04

    • Daemon: ~/laptop-agent/laptop_agent.py on the laptop (canonical copy ~/tools/laptop-agent-daemon.py in the Muse home repo), systemd user service laptop-agent.service (enabled, linger on, Restart=on-failure), binds 100.99.113.103:4799 only, token auth verified (bad token → 401).
    • Client: ~/tools/laptop-agent.py — health|exec|exec-batch|run|poll|kill|jobs; CONNECT handshake reuses the laptop-proxy construction (proxy credential inside the CONNECT request only); agent token sent as a header, kept in mode-600 files on both ends, never in argv or git.

    Measured on the live path (warm SSH mux as baseline):

    workload laptop-agent ssh (warm mux)
    10 commands 1.96 s wall (one request, one CONNECT) 4.38 s wall (10 calls)
    1 trivial command ~1.3 s (cold CONNECT per call) ~0.4 s
    15 s job, polled from separate client processes detached, offset polling, exit code propagated (3) n/a (session-bound)

    Honest read: the agent wins on batched units of work (2.2×) and on long-running jobs (survive client/SSH death; incremental offset polling), which is where the fleet's laptop traffic actually is. For a single trivial command over a warm mux, ssh is still cheaper — the client pays a Python startup plus a fresh CONNECT per invocation. laptop-ssh.sh remains the right tool there.

    Phase 2 (migrate hot call sites) and phase 3 (NixOS-declared service) remain open on this issue.

  3. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    Warm-connection broker added — single calls now at SSH parity (2026-11-04)

    Drew asked for a warm/preconnected path and challenged whether the Python client is the slow part. Measured breakdown of a cold call:

    component cost
    Python interpreter startup ~25 ms (bare), ~80 ms user-CPU with client imports
    tunnel CONNECT handshake (DERP hairpin) ~1,800 ms (variable)
    one request over an open tunnel ~220–350 ms (DERP RTT floor)

    Python is ~5% of the cold call. The handshake is the cost — so the fix is a broker, the ControlMaster pattern applied to the agent:

    • ~/tools/laptop-agent-broker.py (sandbox side): auto-started by the client when its Unix socket is missing; holds a pool of 2 keep-alive HTTP connections over pre-built CONNECT tunnels; keepalive ping every 180 s; exits after 1 idle hour. Broker death verified self-healing: next client call auto-restarts it (2.3 s cold once, warm after).
    • Client routes through the broker by default (LAPTOP_AGENT_NO_BROKER=1 forces the direct CONNECT path).

    Measured after (box heavily loaded; ssh measured in the same window):

    single call before after
    agent health ~1.3–1.4 s ~0.39 s
    agent exec true ~1.3 s ~0.45 s
    ssh true (warm mux) ~0.41 s ~0.41 s

    Parity with warm SSH for single calls, and the earlier advantages stand: batches (10 cmds ≈ 2.0 s in one request) and detached jobs. Remaining floor is the DERP round trip itself (~0.25 s), which no client-side change can remove.

  4. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    Decision recorded (Drew, 2026-11-04 02:17 CST): the laptop agent is the single point of entry for fleet laptop work — one daemon + one warm broker serving 2–3 dozen concurrent sessions, with bare-minimum sandbox impact (the sandbox is underpowered and that is a fixed constraint; CPU cost lives on the laptop).

    Same-night sizing: broker pool 2→12, keepalive idle-exit bug fixed, client fast path slimmed (~35% less client CPU/call). Load test: 30 concurrent execs, zero failures, warm single call ~0.39s, broker CPU negligible. Commits in the home repo: 242cc47c2 (sizing), f3bbbba33 (pool+idle fix).

    Applicability of the Atlas Rust decision (from the Token usage tracking review): the library-path fragility that drove Atlas to Rust does not transfer (this daemon is stdlib-only, no venv/native libs), and the warm broker already killed the cold-cost argument language-independently. The part that does apply is shared-infrastructure status — which this decision now establishes. So: phase 2 (migrate fleet call sites) proceeds on the Python implementation; evaluate the Atlas-pattern port (one nix-packaged Rust artifact, CLI+serve modes, parity-gated) as part of that migration rather than speculatively now. The daemon is ~300 lines of stdlib, so the port stays cheap whenever it's taken.

  5. added
    priority/highImportant and time-sensitive
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    source/humanFiled directly by a human
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Nov 4, 2026
  6. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    Intake review

    Related: issue #5362 (companion queue/secrets brokers design — same session-proxy pattern); issue #2879 (laptop SSH MaxStartups recurrence — the fragility this replaces).
    Notes: Filed by Drew today; the body states the MVP was built alongside the filing, so phase 1 may already be partially verifiable — the first worker pass should re-read what exists on the laptop before building anything. Still live: no merged PR declares the service in dezren39/nix.
    Risks: The daemon is remote-code-execution for token/proxy-credential holders by design; mitigations (tailnet-only bind, token auth, secrets out of argv/git) are acceptance criteria above, not optional polish. A wedged daemon blocks migrated call sites — SSH fallback must stay working.
    Suggested approach: Verify the as-built MVP against the design's API surface first, record the verification on this issue, then file phase 2/3 implementation issues.

    🤖 Agent note · filed

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: repo-fleet-monitor run 2026-11-04 02:12 CST
    • Found via: monitor reconciliation (intake enrichment, protocol §5)
    • Task: intake pass — acceptance criteria appended, labels normalized, this review posted
    • Expectation: done looks like the criteria section above; no queue state changed by this pass
  7. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    Phase 3 progress (2026-11-04): the laptop-agent is now packaged in
    dezren39/nix — pkgs/laptop-agent (vendored source, byte-identical to
    the live daemon, sha256-verified) producing bin/laptop-agent, and
    the services.fleet-brokers home-manager module launches it as a
    systemd user service alongside the two brokers. PR:
    dezren39/darwin-nix#1459 (build + module check
    verified on amd). Activation happens when amd's home configuration
    imports the module; the hand-installed unit keeps running until then.

  8. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 4, 2026
  9. 42 remaining items

  10. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 4, 2026
  11. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    task: 9498
    lane: code-y
    stage: claimed
    execution_lane: direct
    Picked up by lane code-y for phase 2: inventory remaining laptop-ssh.sh call sites, migrate hot ones to laptop-agent, record before/after wall-time — or close the phase by evidence if the remaining sites are all cold/interactive/sudo.

  12. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    task: 9498
    lane: code-y
    stage: implementation_complete
    outcome: phase 2 closed by evidence — no hot unmigrated call sites remain

    Phase 2 inventory: remaining laptop-ssh.sh call sites (2026-11-04, task 9498)

    Searched: ~/tools top-level scripts, active cron definitions (hatch-personal/workspace/cron.d, excluding _archive), hatch-personal/scripts, the repo-queue worker instruction, TOOLS docs.

    Already migrated (the hot paths):

    Remaining sites, classified:

    1. Worker instruction §3 — laptop primary fetch/ff via laptop-ssh.sh, a few calls per direct-lane task. Still-SSH, with reason (measurement below): warm-multiplexed SSH is faster per call than a single agent exec; migration only wins in the batched form.
    2. Worker instruction rate-limit fallback — fleet_paperwork.py token mint via laptop-ssh.sh. Exempt: cold (fires only on a GitHub secondary rate limit) and deliberately a diverse route from the primary path.
    3. ~/tools/oc-attach.py — Exempt: interactive attach, which the design explicitly keeps on SSH.
    4. ~/tools/session-count.py — live session-count probe over SSH. Still-SSH, with reason: single short call per invocation, no cron definition references it; per-call latency favours warm SSH (below).
    5. hatch-personal/scripts/charge-inputs-fetcher/fetch-charge-inputs.py — one SSH fetch per run. Cold.
    6. Cron atlas-index-rewrite-watch (hourly) — one probe/hour for a one-off index rewrite (dezren39/hatch-personal#20). Cold/temporary; dies with the rewrite.
    7. Cron cold-store-eviction-scan (weekly) — runs its whole job on the laptop via SSH, git pull included. Cold (weekly).
    8. dashboard-backup-daily — only in _archive. Dead.
    • TOOLS.md / TOOLS.d/laptop.md mentions are documentation, not call sites.

    Wall-time measurement (2026-11-04 ~15:50Z, sandbox → laptop, laptop load1 ≈ 22)

    Call Median
    laptop-ssh.sh true (warm ControlMaster) 0.274s
    laptop-ssh.sh hostname 0.226s
    laptop-agent.py exec -- true 0.419s
    laptop-agent.py exec -- hostname 0.388s
    laptop-agent.py exec-batch ×5 commands, one invocation 0.294s total
    5× serial laptop-ssh.sh true 1.239s total

    Read: per-call, the agent is ~1.6× slower than SSH — laptop-ssh.sh reuses a persistent master (ControlPersist=3600, the 2026-10-26 fix for #948), so the per-command SSH spawn cost phase 2 targeted is already amortized, while each agent client invocation pays Python startup + CONNECT. The agent's real win is batching (~4.2× on a 5-command step) and run/poll for long jobs — both already the fleet pattern where the work is hot. A blanket per-call migration of the remaining sites would regress, not improve, shift wall time.

    Standing recommendation (recorded, not silently applied): when a lane step needs several laptop probes at once, issue them as one exec-batch instead of serial SSH calls. Papercut found while measuring: laptop-agent.py's docstring shows exec-batch input as {"cmds":[...], "timeout_s":N} without saying each cmd must be a spec object ({"argv":[...]}); raw argv arrays get an HTTP 502 from the broker instead of a validation error.

    Phase 1 was verified complete and phase 3 was rerouted to epic #5412 / design #5425 by Drew's D1 ruling, so with phase 2 closed by this evidence, no #5343-owned work remains — closing the issue.

  13. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    task: 9498
    addendum to the phase 2 inventory above: a follow-up grep of the goal workspace adds the fleet-monitor instruction copies (repo-fleet-monitor-instruction.md and its dated snapshots under hidden_files/) as laptop-ssh.sh instruction-text sites — the monitor's periodic laptop probes. Same class and verdict as worker-instruction §3: still-SSH with reason (per-call latency favours warm SSH; batch multi-probe steps via exec-batch when a step grows). The remaining goal-workspace hits are handoff snapshots, run logs, and docs mirrors — records, not call sites. The classification and conclusion are unchanged.

  14. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    Closing per task 9498: phase 1 verified complete, phase 3 rerouted to epic #5412 / design #5425 (Drew D1), and phase 2 closed by the inventory + wall-time evidence posted above — no hot unmigrated laptop-ssh.sh call sites remain.

  15. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    task: 9498
    addendum 2 (late-arriving grep, for completeness): the goal-workspace cron definitions under keep-the-four-repos-healthy/crons/ also reference laptop-ssh.sh as instruction text — the new-issue-creator family (code a/b, nix, opencode, openchamber), pr-diff-review, the paused issue-sweep-devtoday-code, orchestration-pass, roadmap-sync, action-outbox-processor, branch-cleanup-sweep, and the opencode daily jobs. These are the creator/pr-diff-review laptop calls the phase 2 packet named: a handful of evidence-gathering probes per run, not per-command hot loops. Same class and verdict as worker-instruction §3 in the inventory above — still-SSH with reason (per-call latency favours warm SSH), with the standing recommendation to batch multi-probe steps via exec-batch. The phase 2 conclusion is unchanged.

  16. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    Trip logging v1 live — and a pool correction (2026-11-04)

    (a) Live proxy pool is 50. init-ensure launches fleet proxy serve with LAPTOP_AGENT_POOL=50. The Python broker's in-code default of 12 (~/tools/laptop-agent-broker.py) is only a fallback for when the env var is absent — it is not the live value.

    (b) Trip-log v1 — shipped and verified end-to-end today.

    Correlation: the client mints a short trip id (12 hex chars) per request. It reaches the daemon three ways, in the daemon's resolution order: the X-Trip-Id header (direct connections) → a trip field in the JSON body for POST /exec and POST /jobs → daemon-generated. The body carrier is the one that survives the fleet session proxy, which forwards bodies verbatim but not arbitrary socket-protocol fields; the Python broker also forwards the id as the header when it is the serving broker.

    • Sandbox client log, ~/.local/state/laptop-agent/trips.jsonl — one line per request: {ts, trip, path, total_ms, status}.
    • Laptop daemon log, ~/.local/state/laptop-agent/trips.jsonl — one line per exec item and per detached-job completion: {ts, trip, kind ("exec" | "job"), cmd0, cmd_sha256, duration_ms, exit, out_bytes, err_bytes}.
    • Safety: only the command's first token (cmd0) plus the sha256 of the full command string are recorded — never raw command text, environment, headers, or token values. Perf: a single O_APPEND write per event on each side, wrapped so a logging failure can never fail or visibly delay the request.
    • Verified 2026-11-04 after deploying the daemon and restarting laptop-agent.service (healthy): echo trip-v1-ok produced trip aedc705d4294 on both sides (client total_ms 445, daemon duration_ms 1, exit 0, out_bytes 11); false logged with exit 1; a detached true job logged kind: "job" at completion with the creation request's trip id. Sources committed in the home repo as 74d9c24472695cde203ef421acbe4f3dcda7e8ef; pre-edit daemon copy kept on the laptop at ~/laptop-agent/laptop_agent.py.bak-2026-11-04-pretrip.

    (c) Follow-on, not yet built: ingest of the laptop-side trips.jsonl via the fleet-feed collector into Supabase, so the two sides join by trip id without reading files by hand.

    (d) Rust port direction (Drew, 2026-11-04): porting the daemon to Rust buys a single portable artifact that guarantees full control and minimum overhead — no dependency on the laptop's ambient Python interpreter. The port will carry this trip-log format unchanged.

  17. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    SSH shim retired — laptop-ssh migration record (2026-11-04)

    On Drew's order, ~/tools/laptop-ssh.sh is retired as an SSH transport. Full inventory: keep-the-four-repos-healthy/hidden_files/laptop-ssh-migration-inventory-2026-11-04.md.

    Baseline (md5-verified by three independent reads): the pre-retirement shim was md5 95f647590274779bcaef7ea9a66f0670 (plain SSH wrapper, ControlMaster/ControlPersist 3600). It is preserved byte-identical as ~/tools/laptop-ssh-real.sh, reached only via LAPTOP_SSH_REAL=1 (daemon-down recovery) or the new shim's automatic fallback.

    Fleet-binary finding: fleet laptop-agent in the deployed build (0.1.0, rev 18f0aa1) is a registry stub — exit 69, "lands in a later step of epic #5412" (crates/fleet/src/main.rs, CapState::Available; no fleet-laptop-agent crate exists). The shim therefore delegates to the working client of the fleet laptop-agent path, ~/tools/laptop-agent.py over fleet proxy serve (pool 50); the shim header documents the one-line flip when the binary domain lands.

    Daemon defect found by verification, fixed: shell specs ran /bin/bash -lc — /bin/bash does not exist on NixOS, so daemon shell mode had never worked (argv consumers masked it). Bash is now resolved from PATH/profile locations; deployed both sides at md5 5cb36100dd86ec0df222e45236012222, service restarted, health verified. Backup: laptop_agent.py.bak-2026-11-04-preshellfix.

    New shim shape: exec-style calls delegate to the agent (single command string; multi-arg joined as ssh joined them; bash -s/python3 - stdin scripts converted to the agent shell string over JSON — no heredoc mangling). Exit codes propagate (verified 0/7/3/42). Automatic real-SSH fallback for: ssh options/forwards, interactive use, and stdin carrying DATA (sudo -S feeds, cat > file pushes — the daemon has no stdin channel). Trip log grew exactly by the 5 delegated test calls; fallback calls added none.

    Migrated to the agent directly (verified live): ~/tools/oc-attach.py discovery call (oc-attach discover exit 0) and ~/tools/session-count.py fetch (live tally, exit 0). Docs updated to the end state: TOOLS.d/laptop.md, nixpkgs-update SKILL.md, fleet-build-lock usage example.

    PATH: init-ensure.sh now recreates the ~/tools/bin/fleet / repo-queue symlinks if missing or dangling (verified by delete-and-ensure). Bare login shell resolves both.

    Residuals (stay on the shim, reasons in the inventory): (1) charge-inputs fetcher's stdin-data push — no daemon stdin channel; (2) 21 live cron bodies + goal runbook instruction files — textual migration owed as a coordinated batch with the parallel repo-queue → fleet queue body migration (fresh cron.view before any cron.update); the shim already routes all of them to the agent; (3) port-forwards/interactive ssh shapes — permanently the fallback's job (oc-attach's in-file -N/-L forward is the standing example); (4) JOB-BODY.md left untouched this run to avoid colliding with the parallel migration's quota-watch edits.

    Commits: home repo 581e99ced (shim + daemon, via snapshot) and b8f3a5944; hatch-personal 5a318e420.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

effort/lLarge: multi-day or open-ended; expect staged check-inskind/designA design proposal (design-architect): multi-step, phased, decision-bearingkind/infrastructureInfrastructure / host-portability workkind/performancePerformance problem or optimizationmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/highImportant and time-sensitivesource/humanFiled directly by a humantask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions