Repository navigation
A standalone worker serves agent-device on localhost by default #957
Description
Part of #952.
Scope
A standalone worker serves agent-device on 127.0.0.1:4701 with no config set. On a worker with no gateway.url, http.enabled defaults to on, and agent-device is on unless capabilities.agent-device.enabled is set. With the profile from #956, an agent on the Mac connects and works with no operator step. A Mac without agent-device is not flagged as a problem. Joined workers and gateways keep today's defaults. The test harnesses keep HTTP and agent-device off unless a test asks for them. The docs drop the enable steps.
In short
After an upgrade, a standalone worker listens on 127.0.0.1:4701. The API and the console both still ask for a token. The worker also starts agent-device when the agent-device command is on PATH, and writes the profile from #956. A value the operator set wins. If agent-device was never set and is not installed, its capability is off: simlock doctor stays clean and the console shows no warning. Any other agent-device trouble stays loud, as today.
flowchart LR
L[config load] --> J{gateway.url set?}
J -->|no: standalone| D1["http.enabled default true; agent-device on unless set (changed)"]
J -->|yes: joined| D2[both off, as today]
D1 --> S{agent-device command found?}
S -->|yes| R[daemons start; profile written]
S -->|no, never set| O["capability off, no finding (changed)"]
S -->|no, set true| U[unavailable + finding, as today]
Defaults:
| Key | Standalone worker today | After | Joined worker | Gateway |
|---|---|---|---|---|
http.enabled |
false |
true |
false (unchanged) |
true (unchanged) |
capabilities.agent-device.enabled |
false |
on unless set | false (unchanged) |
false (unchanged) |
User session, a fresh standalone worker with agent-device on PATH and no config.json:
$ simlock daemon start
$ agent-device connect --remote-config ~/.simlock/agent-device-profile.json --session "$CLAUDE_CODE_SESSION_ID"
$ agent-device open --platform ios --session "$CLAUDE_CODE_SESSION_ID"Today the connect fails, because there is no profile file and nothing listens on 4701. After this task the open leases and opens a simulator.
If this goes wrong, a user notices one of three things: the worker still needs config set first; a joined worker refuses to start with the http.enabled error; or doctor and the console complain on a Mac without agent-device.
Technical spec
Commit for every source: cac23e033f94ad87aee0b92e130190094df6d93a.
Facts
| # | Fact | Source |
|---|---|---|
| F1 | Every optional module's enabled defaults to false, forced after its own defaults; core does not name agent-device |
src/core/config.ts:894; src/core/AGENTS.md "Never imports" |
| F2 | http.enabled defaults to role === "gateway"; a worker's port is 4701 and its host 127.0.0.1 |
src/core/config.ts:941-944 |
| F3 | Defaults, the file and overrides are merged; a worker then runs validateGatewayMembership, which refuses http.enabled true with gateway.url set, with ADR 0039's message |
src/core/config.ts:590-597, :800-808 |
| F4 | The module host reads capabilities.<name>.enabled: when it is false or the section is missing, nothing of the module loads and the capability is off |
ADR 0030 §2; src/agent-device/edge.test.ts:4271 |
| F5 | The module's worker part gets only its merged section (ctx.config) |
src/agent-device/worker.ts:110-130; src/modules/types.ts:131-133 |
| F6 | The supervisor finds the command on PATH at start; when it is not found the state is unavailable, reason not-found, and nothing retries until a restart. Other reasons: not-agent-device, wrong-version, state-dir, start-failed |
src/agent-device/host/agent-device-supervisor.ts:32, :269-270, :322-327; src/agent-device/AGENTS.md Availability |
| F7 | agentDeviceFindings makes one module-unavailable doctor finding for any unavailable state; doctor findings carry no severity |
src/agent-device/host/availability-findings.ts:4-17; src/core/doctor.ts:131-134 |
| F8 | A capability is available, unavailable (with a reason) or off (no reason) |
src/contract/schemas.ts:1279-1290 |
| F9 | The console shows unavailable in warn tone and off as idle |
ui/src/model/worker-page.ts:467 |
| F10 | Plain simlock status on a worker prints no capabilities; simlock status --json (status.get) carries them |
src/cli/format.ts:33-40, :230 |
| F11 | An HTTP bind failure after a good start stops the daemon (http-start-failed), exit 1 |
src/daemon/main.ts:835; src/daemon/AGENTS.md |
| F12 | e2e seedConfig has three bases: fake driver merges FAKE_LANE_BASE_CONFIG (no http, no capabilities), real driver merges only drivers.android, none merges nothing |
e2e/helpers/env.ts:90-97, :217-227 |
| F13 | Fast e2e tests set http.host/port without enabled (e.g. e2e/contract-surface.test.ts:110); real-driver slow files slow-ios-smoke, slow-ios-slim, slow-ios-os-range, slow-android-smoke, slow-warm-pool set no http |
grep -c 'http:' on each → 0 |
| F14 | The console lane writes http (enabled unless joined) and no capabilities |
e2e/console/fixtures.ts:239-255 |
| F15 | In-process tests call startDaemon( directly in src/agent-device/edge.test.ts, src/agent-device/gateway-front.test.ts, src/daemon/{optional-modules,server,main,gateway-modules}.test.ts, src/daemon/{lease-requires,gateway-fleet,stream-gateway,worker-capabilities}.e2e.test.ts and src/daemon/test-relay-support.ts; main.test.ts's start() sets no http and uses the real module registry |
grep -rl "startDaemon(" src; src/daemon/main.test.ts:91-114 |
| F16 | Tests asserting today's defaults: config.test.ts:123 (http: { enabled: false, … }), :1663 (worker HTTP off), definition.test.ts:35 (agent-device enabled false), edge.test.ts:4271 (no section → off) |
those lines |
| F17 | The doctor seam that sees findings and the capability together is src/daemon/main.test.ts describe("doctor.run"), case "both daemons down: agent-device is found nowhere" |
src/daemon/main.test.ts:3319-3420, :3366-3372 |
| F18 | A launchd or systemd service saves PATH at install, so a service-run daemon finds the same agent-device |
src/ports/service-manager.ts:34-45 |
| F19 | Public pages state today's defaults or enable steps: reference/configuration.mdx:217, :307; worker/configuration.mdx:92, :113-116; worker/console.mdx:16, :27-31; using-devices/remote-agents.mdx:14-22; using-devices/agent-device.mdx:35-36; website/src/site/content.ts:334 |
the files at the commit |
| F21 | config.get answers the loaded Config as is |
src/daemon/dispatcher.ts:879 |
| F22 | Until the supervisor's first start settles, the capability reads unavailable, reason starting |
src/agent-device/worker.ts:91-96, :145-150 |
| F20 | The slow-lane agent-device suite runs one shared daemon with HTTP and agent-device set on explicitly and mints a token per agent | e2e/slow-agent-device.test.ts:136-146; e2e/helpers/agent-device.ts:99-171 |
Modules touched
Searches: http.enabled readers, enabled: false in defaultConfig, OptionalModuleDefinition, agentDeviceFindings, module-unavailable, startDaemon( (F15), seedConfig, http: in e2e/.
flowchart LR
C["core/config.ts (changed: http default by gateway.url)"] --> H["modules host/types (changed: on-by-default for standalone, set-or-default bit)"]
DEF["agent-device/definition.ts (changed: declares on-by-default)"] --> H
H --> AD["agent-device worker + availability (changed: not-found by default → off)"]
AD --> DR[core/doctor.ts]
HR["e2e/helpers/env.ts, e2e/console/fixtures.ts, in-process test helpers (changed: pin off)"] --> C
Files: src/modules/types.ts (an optional definition may declare enabledByDefault: "standalone-worker", plain data), src/core/config.ts (after the merge: the http.enabled default follows gateway.url; for every module entry that declares enabledByDefault and whose enabled no layer sets, a worker with no gateway.url gets enabled: true; loadConfig's callers also get the set of module names whose enabled came from a default, outside Config; core never names agent-device), src/daemon/main.ts and src/modules/host.ts (pass that set to the module, e.g. enabledByDefault: boolean on WorkerContext), src/agent-device/definition.ts, src/agent-device/worker.ts and host/availability-findings.ts or agentDeviceCapabilitySource (not-found while on by default → off), e2e/helpers/env.ts, e2e/console/fixtures.ts, the F15 test helpers, e2e/slow-agent-device.test.ts and e2e/helpers/agent-device.ts, and the F16 tests. Manuals: src/http/AGENTS.md (State table), src/agent-device/AGENTS.md ("Off unless … (false)"), src/modules/AGENTS.md, the config.ts:937-940 comment. Website: the F19 pages.
Contract and event changes
None. config.get returns Config unchanged (src/daemon/dispatcher.ts:879), so it shows the resolved enabled (true on a standalone worker that never set it). The set of defaulted module names is not part of Config. No event.
Other code on the same state
Searches: readers of http.enabled (src/daemon/main.ts, requireJoinedWorkerHttpOff, requireGatewayHttp) and of capabilities.agent-device (module host, supervisor, doctor, status.get).
requireJoinedWorkerHttpOff(F3): the default isfalsewhengateway.urlis set, so a joined worker never trips it; an explicittruestill does.simlock config setwrites only the key it is given, so the next start re-derives a default.- A worker that later joins (
gateway.urlset) has both off at its next start, and A standalone worker writes a private agent-device profile for local agents #956 withdraws the profile. - A standalone worker writes a private agent-device profile for local agents #956 writes the profile when HTTP is on and agent-device is turned on. After this task that includes a standalone Mac where agent-device is on by default but not installed: the profile and its token are written there too (ADR 0046 Consequences), and do nothing until agent-device is installed and the worker restarts.
Failure modes
| Call | Outcome | What happens | Limit, counted from |
|---|---|---|---|
| HTTP bind on 4701 | ok | listening | none |
| same | taken (EADDRINUSE) | daemon stops, http-start-failed, exit 1, as with HTTP on today (F11) |
none |
| agent-device lookup | found | daemons start as today | 15 s ready wait (today) |
| same | not found, never set | capability off (both roles), no doctor finding |
none |
| same | not found, set true |
unavailable, today's finding |
none |
| same | wrong version, not agent-device, state dir, start failed, never set | unavailable, today's finding |
none |
This task writes no state. Two daemons on one machine with default HTTP clash as in the taken row, which is why the harnesses pin HTTP off.
Rules in play
- ADR 0046 §1: the defaults, the set-or-default bit outside the contract, and quiet only for not-found, exactly as written. It narrows ADR 0023 §1 and ADR 0030 §2 for this module only.
- ADR 0039 "invalid …
gateway.urlset andhttp.enabled: true": kept; a default never produces it. - ADR 0011 §3 "the console is served whenever HTTP is": the console now opens on a standalone worker by default and still asks for a token.
- Architecture: core names no agent-device; the on-by-default flag is a neutral member of the module definition.
- Documentation rule 9: the F19 pages change in this PR.
- Slow lane only where the fast lane cannot prove it: one case, the real CLI with the profile.
Tests
Seams: loadConfig (model src/core/config.test.ts); a whole worker daemon (src/daemon/main.test.ts describe("doctor.run") for findings, src/agent-device/edge.test.ts "a worker's capabilities in status.get" for the capability); slow lane e2e/slow-agent-device.test.ts.
- A worker with no config file loads with
http.enabledtrue. Cases: standalone → true; joined (gateway.urlandgateway.tokenset) → false; gateway → true; standalone withhttp.enabled: falsein the file → false; joined withhttp.enabled: truein the file → refused with ADR 0039's message. Extendsconfig.test.ts:123,:1663and:1835. - A standalone worker with no
capabilities.agent-devicesection runs agent-device; a joined worker and a gateway do not. Cases: no section, standalone →available;enabled: false→off; no section, joined →off. Extendsedge.test.ts:4271anddefinition.test.ts:35. - agent-device trouble while on by default (doctor.run, extends the
main.test.ts:3366cases): not found, never set → no agent-device finding, capabilityoff; not found, settrue→ today's finding,unavailable; wrong version, never set → today's finding,unavailable. - A standalone worker on its defaults serves agent-device end to end (in-process
startDaemonwith the fake agent-device, modeledge.test.ts): config sets onlyhttp.port(a free port) andcapabilities.agent-device.command(the fake), noenabledkeys → HTTP listens,config.getshows bothenabledtrue, the A standalone worker writes a private agent-device profile for local agents #956 profile is written, andGET /v1/agent-device/healthwith the profile's token answers 200. The same withgateway.urlandgateway.tokenset → no HTTP, no profile, capabilityoff. - The harnesses give HTTP and agent-device to no test that does not ask: the fake and real
seedConfigbases and the console lane config sethttp.enabled: falseandcapabilities.agent-device.enabled: false, which a test's own values override; every F15 helper does the same. A fast e2e test that setshttp.portand uses HTTP setsenabled: true. The fast e2e files the change touches, and the F15 files, pass. - Slow lane,
e2e/slow-agent-device.test.ts: a new case starts the pinned agent-device CLI with noAGENT_DEVICE_DAEMON_AUTH_TOKENand runsconnect --remote-config <data dir>/agent-device-profile.json --session <name>,open --platform ios,installof the fixture app (an upload) andclose, each with--session. The lease's owner is the profile token. The existing token cases keep passing on the same daemon.
Done when
pnpm exec vitest run --project unit <file>passes forsrc/core/config.test.ts,src/agent-device/definition.test.ts,src/agent-device/edge.test.tsandsrc/daemon/main.test.tswith the lines above. The doctor.run cases wait for the capability to leavestartingbefore reading it.pnpm test:e2epasses for the e2e files the harness change touches. CI is green.- Slow lane:
scripts/slow-e2e.sh e2e/slow-agent-device.test.tspasses, including the profile case. It needs the "agent-device through simlock" machine. - The website build passes.
using-devices/agent-device.mdxsays a standalone worker serves agent-device by default, drops theconfig setsteps, and says to restart the worker after installing agent-device. The other F19 pages state the new defaults.
Out of scope
- The profile file: A standalone worker writes a private agent-device profile for local agents #956.
- Running without HTTP when 4701 is taken (ADR 0046 Alternatives).
- The shipped agent instructions (Add
simlock instructions: the rules an agent must follow, printable and served over MCP #147).
Depends on
Written by an agent.
Activity
test: a worker with no config file loads with http.enabled true unles…
test: a standalone worker with no capabilities.agent-device section r…
test: agent-device not found while on only by default is quiet and th…
feat(agent-device): a command never found while on only by default le…
test: the harnesses give HTTP and agent-device to no test that does n…
test: an agent connected with the worker's profile file opens, instal…
docs: a standalone worker serves HTTP and agent-device by default (#957)
refactor(core): simplify the defaulted-modules return and the doctor …
refactor(core): simplify the doctor test's scripted daemons and a sta…
fix(e2e): a console worker's own capabilities keep the agent-device p…
fix(agent-device): the local profile carries a constant clientId agen…
feat(core): a standalone worker serves agent-device on localhost by d…