Skip to content

Install an agent from the picker, on this Mac as well as on a box - #219

Merged
pallaoro merged 4 commits into
mainfrom
this-happens-on-mobile-remote-box
Sep 5, 2026
Merged

pallaoro merged 4 commits into
mainfrom
this-happens-on-mobile-remote-box

Conversation

@pallaoro

@pallaoro pallaoro commented Sep 5, 2026

Copy link
Copy Markdown
Member

Follow-up to #217. Asked as "why is the option not showing the Install button if I wanted to install it directly from Ateam?"

Why there was no button

The whole flow was SSH, so it could only ever mean "a box". installAgent(alias: string, agentId) (shared/host.ts:163) cannot express the local machine, and the implementation is ssh <box> "bash -lc '<install> && command -v <bin>'" (main/host.ts:597). The picker suppressed the button accordingly: canInstall = !avail && alias !== null && …. That shape came from #108, "install coding agents on a box", and left this Mac and the phone with no way in.

SSH was never a constraint here: installAgent already requires a connected backend (host.ts:594) and runs after the engine is up (host.ts:574-581).

What it is now

An engine RPC, the way editor:install already installs code-server on the task's own machine (dispatcher.ts, aggregate.ts). The engine doing the installing is the machine that has to end up with the binary, so one implementation serves this Mac, a box, and eventually the phone, which has never had an install path. Routed by the engine's projectId, the key that already lands a task on a given environment (unify.ts).

Installing keeps && command -v <bin> as its acceptance test, so a "done" cannot contradict the next launch, and reports the installer's own last lines when it fails, as installCodeServer does.

The half without which the button would lie

ensureLoginEnv resolves the PATH once at startup and returns on the first success (engine.ts:137, login-env.ts:165-168); the retry backoff exists only for a probe that failed. So the PATH this process spawns everything with is a snapshot of the machine as it was when the app opened, and a CLI installed afterwards is invisible until a restart, whoever installed it.

Seen for real: opencode's installer appends to ~/.zshrc, so a fresh interactive login shell finds it and the running app cannot.

refreshLoginPath re-resolves and re-adopts, rate-limited because an interactive login shell can take PROBE_TIMEOUT_MS to answer. Two call sites, and it needs both:

  • the agent catalog, because a greyed-out row is unselectable, so no launch can ever be attempted to discover the agent has arrived;
  • the launch guard before it refuses, because a loop tick never opens the picker that would otherwise have refreshed it.

Verification

  • Typecheck clean. 413 pass / 0 fail, run both normally and with agent CLIs stripped from PATH (the CI condition).
  • Nine new tests; the two guard tests proven non-vacuous by disabling the refresh and watching them fail.
  • Live on a Mac whose PATH predates the install, no stubs: probe → absent, refreshLoginPath → changed, probe → present, and a real spawnAgentInTask("opencode") spawns and files the card running. No restart.
  • installAgentCli exercised against a synthetic agent: resolves when the binary lands, rejects when command -v doesn't find it.

Three things the build corrected along the way: box installs do stream today, so dropping streaming was a regression rather than a limitation (followed installCodeServer instead: spinner on success, installer's last lines on failure); a test caught two clocks fighting in my own rate limiter; and dispatcher.test.ts didn't stub the refresh, so it ran a live login shell and mutated the test process's PATH, breaking three later tests that shell out to git.

A launch runs `<agent>; exec $SHELL -l` in a PTY, so a missing binary
became one line of shell output in a pane that then looked healthy: the
card was filed `running`, a loop tick reported "run N started", and
nothing said the agent never ran. An agent installed yesterday and gone
today (an uninstall, a PATH that moved under a reboot) failed silently
every hour.

Every launch now asks the login shell whether the binary resolves before
anything is written down. The probe uses the same shell and flags the
launch does, because probing /bin/sh with the daemon's own PATH answers a
different question and produces false absents on exactly the Macs whose
boot-time PATH probe missed. It reports present/absent/unknown and only a
definite absent refuses: a shell that fails to answer knows nothing about
the binary, and blocking work on that would be worse than the bug.

Three callers did irreversible bookkeeping before spawning, which a
refusal would have stranded:
  - a loop persisted its task link only after the launch, so a failing
    tick left it unlinked and the next one built another worktree;
  - restore stamped the tab `restored` first, and that is a one-way door
    off the restorable strip;
  - the loop form's <select> omitted an uninstalled agent, so it showed a
    different one while saving the missing one straight back.

Also gates the phone's agent picker, which offered agents the box hasn't
got while the desktop's has always disabled them.

The probe is a seam on Services so tests don't assert which CLIs happen
to be installed on the machine running them (a CI runner has none).
The Install button only ever appeared for a box, because the whole flow was
SSH: `installAgent(alias, agentId)` couldn't express the local machine, and
its implementation was `ssh <box> "bash -lc '<install> && command -v <bin>'"`.
That shape came from #108, "install coding agents on a box", and it left the
Mac and the phone with no way in at all.

It is now an engine RPC, the way editor:install already installs code-server
on the task's own machine: the engine doing the installing IS the machine that
has to end up with the binary, so one implementation serves this Mac, a box,
and the phone. Routed by the engine's projectId, the key that already lands a
task on a given environment. SSH was never a constraint here: installAgent
already required a connected backend and ran after the engine was up.

The button would have lied without the second half. `ensureLoginEnv` resolves
the PATH once at startup and returns on the first success, so the PATH this
process spawns everything with is a snapshot of the machine as it was when the
app opened. A CLI installed afterwards is invisible until a restart, whoever
installed it. Seen for real: opencode's installer appends to ~/.zshrc, a fresh
login shell finds it, the running app cannot.

So `refreshLoginPath` re-resolves and re-adopts, rate-limited because an
interactive login shell can take ten seconds to answer. Two call sites, and it
needs both:

  - the agent catalog, because a greyed-out row is unselectable, so no launch
    can ever be attempted to discover the agent has arrived;
  - the launch guard before it refuses, because a loop tick never opens the
    picker that would otherwise have refreshed it.

Verified on a Mac whose PATH predates the install: probe absent, refresh finds
it, re-probe present, launch spawns. No restart.

Installing keeps `&& command -v <bin>` as its acceptance test, so "done" can't
contradict the next launch, and reports the installer's own last lines when it
fails.
@pallaoro
pallaoro enabled auto-merge (squash) September 5, 2026 14:57
@pallaoro
pallaoro merged commit ebf78a2 into main Sep 5, 2026
1 check passed
@pallaoro pallaoro mentioned this pull request Sep 5, 2026
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.

1 participant