Repository navigation
Description
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 /healthPOST /exec— a batch of commands in one request, per-command exit/stdout/stderr/duration, bounded timeout + output capsPOST /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 fromHTTPS_PROXY, inside the request only), subcommandshealth|exec|run|poll|kill|jobs. laptop-ssh.shstays for interactive/sudo use; the daemon is additive. Fleet rules unchanged (no sandbox worktrees; builds on laptop/CI).
Phases
- MVP — daemon + client + user service; verified end-to-end vs the ssh baseline. (Built 2026-11-04 alongside this filing.)
- Migrate hot call sites — creator/lane laptop probes and test runs move to
run/poll; measure lane shift wall-time before/after. - 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
dezren39 commentedon Nov 4, 2026
Phase 1 (MVP) built and verified — 2026-11-04
~/laptop-agent/laptop_agent.pyon the laptop (canonical copy~/tools/laptop-agent-daemon.pyin the Muse home repo), systemd user servicelaptop-agent.service(enabled, linger on,Restart=on-failure), binds 100.99.113.103:4799 only, token auth verified (bad token → 401).~/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):
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.shremains the right tool there.Phase 2 (migrate hot call sites) and phase 3 (NixOS-declared service) remain open on this issue.
dezren39 commentedon Nov 4, 2026
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:
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).LAPTOP_AGENT_NO_BROKER=1forces the direct CONNECT path).Measured after (box heavily loaded; ssh measured in the same window):
exec truetrue(warm mux)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.
dezren39 commentedon Nov 4, 2026
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.
dezren39 commentedon Nov 4, 2026
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
dezren39 commentedon Nov 4, 2026
Phase 3 progress (2026-11-04): the laptop-agent is now packaged in
dezren39/nix —
pkgs/laptop-agent(vendored source, byte-identical tothe live daemon, sha256-verified) producing
bin/laptop-agent, andthe
services.fleet-brokershome-manager module launches it as asystemd 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.
42 remaining items
dezren39 commentedon Nov 4, 2026
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.
dezren39 commentedon Nov 4, 2026
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.shcall sites (2026-11-04, task 9498)Searched:
~/toolstop-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):
run/pollRemaining sites, classified:
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 agentexec; migration only wins in the batched form.fleet_paperwork.py tokenmint vialaptop-ssh.sh. Exempt: cold (fires only on a GitHub secondary rate limit) and deliberately a diverse route from the primary path.~/tools/oc-attach.py— Exempt: interactive attach, which the design explicitly keeps on SSH.~/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).hatch-personal/scripts/charge-inputs-fetcher/fetch-charge-inputs.py— one SSH fetch per run. Cold.atlas-index-rewrite-watch(hourly) — one probe/hour for a one-off index rewrite (dezren39/hatch-personal#20). Cold/temporary; dies with the rewrite.cold-store-eviction-scan(weekly) — runs its whole job on the laptop via SSH,git pullincluded. Cold (weekly).dashboard-backup-daily— only in_archive. Dead.Wall-time measurement (2026-11-04 ~15:50Z, sandbox → laptop, laptop load1 ≈ 22)
laptop-ssh.sh true(warm ControlMaster)laptop-ssh.sh hostnamelaptop-agent.py exec -- truelaptop-agent.py exec -- hostnamelaptop-agent.py exec-batch×5 commands, one invocationlaptop-ssh.sh trueRead: per-call, the agent is ~1.6× slower than SSH —
laptop-ssh.shreuses 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) andrun/pollfor 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-batchinstead of serial SSH calls. Papercut found while measuring:laptop-agent.py's docstring showsexec-batchinput 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.
dezren39 commentedon Nov 4, 2026
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.mdand its dated snapshots underhidden_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.dezren39 commentedon Nov 4, 2026
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.
dezren39 commentedon Nov 4, 2026
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.dezren39 commentedon Nov 4, 2026
Trip logging v1 live — and a pool correction (2026-11-04)
(a) Live proxy pool is 50. init-ensure launches
fleet proxy servewithLAPTOP_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-Idheader (direct connections) → atripfield in the JSON body forPOST /execandPOST /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.~/.local/state/laptop-agent/trips.jsonl— one line per request:{ts, trip, path, total_ms, status}.~/.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}.cmd0) plus the sha256 of the full command string are recorded — never raw command text, environment, headers, or token values. Perf: a singleO_APPENDwrite per event on each side, wrapped so a logging failure can never fail or visibly delay the request.laptop-agent.service(healthy):echo trip-v1-okproduced tripaedc705d4294on both sides (clienttotal_ms445, daemonduration_ms1,exit0,out_bytes11);falselogged withexit1; a detachedtruejob loggedkind: "job"at completion with the creation request's trip id. Sources committed in the home repo as74d9c24472695cde203ef421acbe4f3dcda7e8ef; 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.jsonlvia 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.
dezren39 commentedon Nov 4, 2026
SSH shim retired — laptop-ssh migration record (2026-11-04)
On Drew's order,
~/tools/laptop-ssh.shis 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 viaLAPTOP_SSH_REAL=1(daemon-down recovery) or the new shim's automatic fallback.Fleet-binary finding:
fleet laptop-agentin 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.pyoverfleet 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 md55cb36100dd86ec0df222e45236012222, 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 > filepushes — 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.pydiscovery call (oc-attach discoverexit 0) and~/tools/session-count.pyfetch (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-queuesymlinks 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 queuebody 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) andb8f3a5944; hatch-personal5a318e420.