Repository navigation
agent-device on the same Mac works with no operator step #952
Description
Request: #950
Problem
agent-device is the primary way agents are meant to use Simlock, and it is the path with the most setup. Before the first agent-device open on the same Mac as the worker, an operator turns on capabilities.agent-device.enabled and http.enabled, restarts the daemon, and mints one agent token per agent. The agent exports the token and runs connect proxy against a URL the docs never state for localhost. A plain simlock lease needs none of this: the local socket identifies the user and the Claude Code or Codex session id identifies the agent.
Who it is for
A developer with one Mac running a standalone Simlock worker, whose coding agents (Claude Code, Codex, any CLI agent) drive simulators and emulators through the agent-device CLI on that same Mac.
Outcome
On a standalone worker with agent-device installed, an agent connects with one command that names a private file, then opens, uses and closes a device. Nobody runs a config set, restarts the daemon or mints a token first. Only the Mac user who runs the worker can read that file. Remote agents, joined workers and gateways work as today.
How it works for the user
flowchart TD
S[simlock daemon start, standalone worker] --> H[HTTP on 127.0.0.1:4701, agent-device on]
H --> P[writes ~/.simlock/agent-device-profile.json, readable by this user only]
A[Agent, same user] -->|agent-device connect --remote-config profile --session id| C[connected]
C -->|open / install / close, each with --session| D[device leased and driven]
X[Other account or a browser page] -.cannot read the file.-> E[401 as today]
N[agent-device not installed] --> Q[capability off, doctor quiet]
Before (the operator turned both settings on, restarted, and ran simlock token create --role agent):
$ export AGENT_DEVICE_DAEMON_AUTH_TOKEN=slk_…
$ agent-device connect proxy --daemon-base-url http://127.0.0.1:4701/v1/agent-device
$ agent-device open --platform iosAfter (a fresh standalone worker, agent-device on PATH, no operator step):
$ 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"
$ agent-device install ./MyApp.app --session "$CLAUDE_CODE_SESSION_ID"
$ agent-device close --session "$CLAUDE_CODE_SESSION_ID"All local agents share the profile's token. What keeps them apart is the agent-device session, so every command passes the agent's own --session. agent-device 0.21.28 runs a command with no --session flag in the session that connected last, which may be another agent's (dist/src/cli.js, allowActiveFallback; #931).
After the profile token is revoked and the worker restarts with a new one, agent-device answers Active remote connection config changed. Run agent-device connect --force to refresh it., and the agent reconnects with --force.
Examples
Defaults:
| Key | Standalone worker today | After | Joined worker | Gateway |
|---|---|---|---|---|
http.enabled |
false |
true (127.0.0.1:4701) |
false (unchanged) |
true (unchanged) |
capabilities.agent-device.enabled |
false |
on unless set | false (unchanged) |
false (unchanged) |
<data dir>/agent-device-profile.json |
none | written when HTTP and agent-device are on | none | none |
agent-device on a standalone worker that never set capabilities.agent-device.enabled:
| agent-device on the Mac | Today | After |
|---|---|---|
| installed, right version | off | available |
not on PATH |
off | off, no doctor finding |
on PATH, too old |
off | unavailable, doctor finding |
set enabled: true, not on PATH |
unavailable, doctor finding |
unavailable, doctor finding |
What could go wrong
- Something else already listening on port 4701 stops the worker from starting after the upgrade, as an explicit
http.enabled: truedoes today. The operator setshttp.portor turns HTTP off. - Local agents see each other's leased devices in
agent-device devices. An agent that leaves--sessionoff a command, or reuses another's name, drives the other agent's device, and nothing refuses it. - Any program running as the same Mac user can read the profile and act as a local agent. The Unix socket already has the same boundary.
- Revoking the profile token cuts every local agent off until the next worker start.
Words used
- standalone worker: a worker daemon with no
gateway.url, e.g. a developer's Mac. A joined worker hasgateway.urlset. - profile: agent-device's remote-config file (
--remote-config), here~/.simlock/agent-device-profile.json. It holds the worker's agent-device URL and a token. - profile token: the agent token the worker keeps in the profile, labelled
agent-device on this Mac. - session: agent-device's
--sessionname. Each agent picks its own, e.g. its Claude Code session id.
Non-goals
- No change for remote agents, joined workers or gateways.
- No change to how Simlock checks credentials: the profile carries an ordinary agent token.
- No per-agent token or isolation: local agents share the profile token.
- No change to agent-device itself.
- No fallback when port 4701 is taken.
- The shipped agent instructions (Add
simlock instructions: the rules an agent must follow, printable and served over MCP #147) are written there, not here.
Completion conditions
- A standalone worker with HTTP and agent-device on writes the profile at mode 0600, keeps the same bytes across restarts, and removes it when joined or turned off.
- With the profile, a real agent-device CLI with no token in its environment connects, opens an iOS simulator, installs an uploaded app and closes it (slow lane).
- A worker with no
config.jsonand nogateway.urlstarts with HTTP on and agent-device on. A joined worker starts with both off. - A standalone worker without agent-device on
PATHstarts, andsimlock doctorlists no agent-device finding. - The agent-device docs page gives the profile connect line,
--sessionon every command, and theconnect --forceremedy, with noconfig setsteps.
Open questions
none
Decisions
- ADR 0046 — A standalone worker serves agent-device by default and hands local agents a private profile (PR docs(adr): 0046 a standalone worker serves agent-device by default with a private profile #954): the defaults, the quiet not-found, the profile file, its token and its stability.
Shared values
| Value or state | Rule | Written by | Read by | Source |
|---|---|---|---|---|
| Standalone worker | a worker with no gateway.url |
today's config | #956 (write or withdraw), #957 (defaults) | src/core/config.ts:800-806 |
| Profile file | <data dir>/agent-device-profile.json, mode 0600, atomic; content and key order as ADR 0046 §2 (a wildcard host → 127.0.0.1); written by the module host after the socket claim and before HTTP listens, whenever HTTP is on and agent-device is turned on (set or by default, found or not); deleted otherwise; rewritten only when its bytes change |
#956 | agent-device CLI; #957 defaults test and slow-lane case | ADR 0046 §2–3 |
| Profile token | an agent token labelled agent-device on this Mac; reused while it verifies; every other token with the label is revoked at each start, so at most one is live |
#956 | /v1/agent-device (today's token check), token.list |
ADR 0046 §3 |
| Profile prepare failure | warn Could not prepare the agent-device profile: <message>; the start goes on |
#956 | #956 tests | ADR 0046 §3 |
http.enabled default |
true on a standalone worker, false on a joined one, true on a gateway; a set value wins |
#957 | daemon start, #956 (when to write) | ADR 0046 §1; ADR 0039 |
| agent-device on by default | the definition declares it; the config loader sets enabled: true on a standalone worker when no layer sets it, so config.get shows true; the names of defaulted modules travel beside Config to the module |
#957 | module host, the agent-device module, #956 (when to write) | ADR 0046 §1 |
| Quiet not-found | not found while on by default → capability off, no doctor finding; every other failure, or not-found when set, → unavailable with today's finding |
#957 | doctor, status.get, console |
ADR 0046 §1 |
Behaviours
| Behaviour (invoker, action, what it sees) | Seam | Cases | Task |
|---|---|---|---|
| A worker with HTTP and agent-device on starts and writes the profile; a request with its token reaches agent-device | in-process worker startDaemon with the fake agent-device (src/agent-device/edge.test.ts) |
mode 0600; content and order; URL for host 127.0.0.1, 0.0.0.0, ::, ::1; one labelled agent token; allocate granted, owner the token id |
#956 |
| The worker restarts | same | nothing changed → same bytes and token; port changed → URL rewritten, same token; token revoked, file deleted, file not JSON, token unlabelled → new token, others revoked | #956 |
| The profile does not apply | same | HTTP off (always so on a joined worker), agent-device off → no file, no labelled token; gateway → none | #956 |
| Preparing the profile fails | same | write refused → worker starts, one warn line | #956 |
A worker loads its config with no http.enabled |
loadConfig (src/core/config.test.ts) |
standalone → true; joined → false; gateway → true; explicit false kept; joined with explicit true → ADR 0039's refusal | #957 |
A worker with no capabilities.agent-device section |
whole worker (edge.test.ts status.get) |
standalone → available; enabled: false → off; joined → off |
#957 |
| agent-device trouble while on by default | src/daemon/main.test.ts doctor.run |
not found, never set → off, no finding; not found, set → finding; wrong version, never set → finding |
#957 |
A standalone worker with no enabled keys serves agent-device end to end |
in-process startDaemon (edge.test.ts) |
standalone → HTTP listens, profile written, health with the profile token 200; joined → none of these | #957 |
| A test harness starts workers | e2e/helpers/env.ts, e2e/console/fixtures.ts, in-process helpers |
no test gets HTTP or agent-device without asking | #957 |
| A real agent-device CLI uses the profile | slow lane e2e/slow-agent-device.test.ts |
connect --remote-config, open --platform ios, install (upload), close, each with --session, no token in the environment |
#957 |
Tasks
flowchart LR
T1["#956 private agent-device profile"] --> T2["#957 on by default"]
- A standalone worker writes a private agent-device profile for local agents #956 A standalone worker writes a private agent-device profile for local agents — with HTTP and agent-device on, an agent on the Mac connects with the profile instead of a token. Risk: the file is missing, its token is refused, or it changes on every restart and forces
connect --force. - A standalone worker serves agent-device on localhost by default #957 A standalone worker serves agent-device on localhost by default — no
config setor restart before the first use. Risk: a joined worker refuses to start, or doctor and the console complain on a Mac without agent-device.
Completion conditions:
- Profile written, stable and withdrawn: A standalone worker writes a private agent-device profile for local agents #956.
- Real CLI with the profile (slow lane): A standalone worker serves agent-device on localhost by default #957.
- Defaults on a standalone worker, off on a joined one: A standalone worker serves agent-device on localhost by default #957.
- No doctor finding without agent-device: A standalone worker serves agent-device on localhost by default #957.
- Docs: A standalone worker writes a private agent-device profile for local agents #956 adds the profile section; A standalone worker serves agent-device on localhost by default #957 drops the enable steps.
Open work in the same area:
- agent-device docs: parallel agents need their own --session #931 (bug, agent-device docs:
--sessionper agent): the same page. The docs here state--sessionon every command. No Depends on; the bug stays agent-device's. - Add
simlock instructions: the rules an agent must follow, printable and served over MCP #147 (task:ready,simlock instructions): its body still teaches the token flow (AGENT_DEVICE_DAEMON_AUTH_TOKEN,connect proxy); a comment there points at the profile connect line. Its text is written there, not here. No Depends on. - PR fix(daemon): exec children get a minimal environment; docs say what an agent token can do #891 edits
gateway/access-control.mdxandremote-agents.mdx; A standalone worker serves agent-device on localhost by default #957 editsremote-agents.mdx:14-22. A text conflict only. - PRs fix(daemon): status.get shows an agent only its own leases and queue entries #893 and feat(gateway): a join token is pinned to the first worker that uses it #894 edit
gateway/access-control.mdx; feat(gateway): a join token is pinned to the first worker that uses it #894 also editssrc/auth/index.ts(authenticateUplink),src/auth/AGENTS.mdandsrc/daemon/main.ts(:913). A standalone worker writes a private agent-device profile for local agents #956 edits those files on other lines. Text conflicts only. - Stream any running device to an operator by worker and device id #909's PR feat(stream): stream any running device by worker and device id #923 edits
reference/configuration.mdxnear:311; A standalone worker serves agent-device on localhost by default #957 edits:217and:307. A text conflict only. - Console: the worker's Events tab scrolls back through its history #911 (PR feat(history): the worker's Events tab scrolls back through its history #943) bumps the protocol. Neither task changes a contract shape, so there is no overlap.
Written by an agent.
Activity
V3RON commentedon Oct 23, 2026
Spec updated: Who it is for, Outcome, How it works, Examples, Decisions (ADR 0046, PR #954), Shared values, Behaviours and Tasks (#956, #957) added.
Written by an agent.
V3RON commentedon Oct 23, 2026
Spec updated: the trust model changed from a no-token loopback identity to a private agent-device profile file (ADR 0046 rewritten). How it works, Examples, What could go wrong, Words used, Non-goals, Completion conditions, Shared values, Behaviours and Tasks rewritten; #956 retitled and rewritten; #957 rewritten.
Written by an agent.
V3RON commentedon Oct 23, 2026
Delivery run
Merged: none. The maintainer asked to stop before the gate. The stack #954 → #963 (#956) → #967 (#957) is ready, reviewed, green and up to date with
main.Parked: none
Waiting: the stack's gate (
merge-pr.sh 967), on the maintainerDecided: #956 — must the profile be on disk before HTTP listens? → yes, pinned by a test; #957 — the profile needs a
clientId(agent-device's proxyopenrefuses one without it) → a constant idFound: #971 — the agent-device sharding slow-lane case fails on
maintoo (not caused by this feature)Written by an agent.
docs(adr): 0046 a standalone worker serves agent-device by default wi…
feat(core): a standalone worker serves agent-device on localhost by d…
github-actions commentedon Oct 24, 2026
All 2 sub-issues are closed. If the completion conditions hold on
main, close this feature and flip its ADRs to Accepted.github-actions commentedon Oct 24, 2026
All 2 sub-issues are closed. If the completion conditions hold on
main, close this feature and flip its ADRs to Accepted.