Skip to content

One Simlock instance per host #700

Closed
2 of 2 issues completed
Closed

Description

@V3RON

Request: none

Problem

Today every SIMLOCK_HOME is a separate Simlock. A user can run several workers on one machine, each with its own registry, devices and adb server. None of them can see the others:

  • component remove counts only its own devices, so it can remove a runtime another worker still uses.
  • Two workers can enrol the same physical device, and each release uninstalls the apps the other installed.
  • A console port one worker handed out but has not booted yet reads free to another.

iOS runtimes belong to the whole machine (CoreSimulator keeps one copy in /Library/Developer/CoreSimulator), so no per-folder setup can isolate them. The planned disk lifecycle feature (garbage collection of runtimes, system images and devices with a disk budget) needs one owner of what Simlock installs.

The gateway and the worker are told apart by a mode config key, and running both on one Mac needs two SIMLOCK_HOMEs.

Who it is for

A developer or CI operator who runs a Simlock worker for coding agents on a machine, and sometimes a gateway in front of several workers.

Outcome

  • A user runs one worker, always in ~/.simlock. A second worker for the same user cannot start.
  • A gateway always lives in ~/.simlock/gateway. The folder decides what a daemon is; there is no mode key.
  • On a machine with one daemon, every command reaches it with no flag. On a machine with both, plain commands reach the worker and --gateway reaches the gateway. --local always reaches the worker, which is how a worker is added to a machine that got its gateway first.
  • A worker's HTTP listens on 4701 by default and a gateway's on 4700, so both fit on one machine with defaults.

How it works for the user

flowchart TD
  A["simlock <command>"] --> B{--gateway or --local?}
  B -- "--gateway" --> G["gateway in ~/.simlock/gateway"]
  B -- "--local" --> W
  B -- no --> C{"a gateway is present<br/>and ~/.simlock/instance.json does not exist?"}
  C -- yes --> G
  C -- no --> W["worker in ~/.simlock<br/>(started if not running)"]
  W --> D{"worker refuses: only a gateway does this<br/>and a gateway is present?"}
  D -- yes --> E["same error, plus a hint to add --gateway"]
Loading

A gateway and a worker on one Mac, before and after:

# today
SIMLOCK_HOME=~/.simlock-gw simlock config set mode gateway
SIMLOCK_HOME=~/.simlock-gw simlock daemon start
SIMLOCK_HOME=~/.simlock-gw simlock token create --role worker

# after
simlock --gateway daemon start
simlock --gateway token create --role worker
simlock worker drain 3f2a…
{"error":{"code":"UNSUPPORTED_IN_WORKER_MODE","message":"worker.drain acts on a gateway's workers, and this daemon is not a gateway. A gateway also runs on this machine: run the command again with --gateway."}}

A machine that runs only a gateway:

simlock --gateway config set http.port 4800
simlock daemon start          # no flag: only a gateway lives here, so this starts it
Daemon running
Console: http://127.0.0.1:4800/
simlock status --json | jq .daemon.mode
"gateway"

The old key, after:

simlock config set mode gateway
{"error":{"code":"USAGE","message":"Invalid config after setting mode: \"mode\" is no longer a config key. A daemon started with --gateway is a gateway; any other is a worker. Remove \"mode\" from config.json."}}

Examples

Which daemon a command reaches, with no --gateway flag:

~/.simlock/instance.json gateway present (~/.simlock/gateway/instance.json or config.json) Today After
no no worker in ~/.simlock worker in ~/.simlock
yes no worker worker
no yes worker (the folder means nothing) gateway
yes yes worker worker; a gateway-only command adds the hint
no no, but ~/.simlock/gateway/ exists empty worker worker

With --gateway:

Command Gateway present After
simlock --gateway daemon start no creates and starts the gateway in ~/.simlock/gateway
simlock --gateway daemon start yes starts it, or answers "Daemon running"
simlock --gateway status (any command that would start a daemon) yes, not running starts it, then answers
simlock --gateway status, or any other --gateway command but daemon start and config set no exit 2, code GATEWAY_NOT_PRESENT: No gateway on this machine. Start one with: simlock --gateway daemon start
simlock --local daemon start yes, no worker yet starts a worker in ~/.simlock; plain commands reach it from then on
simlock --gateway config set http.port 4800 no writes ~/.simlock/gateway/config.json; the gateway is now present

What could go wrong

  • A command reaches the wrong daemon: the user sees a worker's answer where they expected the gateway's. Unlikely; the flag always wins.
  • On a gateway-only machine, a plain command starts an empty worker. Unlikely; the table above prevents it.
  • A setup that relied on SIMLOCK_HOME or mode stops working. Certain, accepted: Simlock has no users yet.
  • Two macOS users on one machine each run a worker and share iOS runtimes. Not prevented; written down as a known gap.

Words used

  • worker: the daemon that owns devices, e.g. the one in ~/.simlock.
  • gateway: the daemon that fronts several workers and owns no devices, e.g. the one in ~/.simlock/gateway.
  • root: ~/.simlock, or $SIMLOCK_HOME in tests. The worker lives in the root, the gateway in <root>/gateway.
  • worker marker: <root>/instance.json. A worker writes it on its first start; config set alone never does.
  • present gateway: <root>/gateway/instance.json or <root>/gateway/config.json exists. The gateway writes the first on its first start; simlock --gateway config set writes the second. An empty gateway folder is not one.
  • gateway-only command: one a worker refuses with UNSUPPORTED_IN_WORKER_MODE, e.g. simlock worker drain.

Non-goals

  • Several OS users on one machine, each with a worker (known gap).
  • One daemon that is both gateway and worker.
  • Moving data from an old SIMLOCK_HOME or an old mode key. No migration.
  • The disk lifecycle feature itself. It depends on this one.
  • A new lock: the existing socket claim is what refuses a second worker.

Completion conditions

  • A second worker for the same user cannot start: it finds the running one at ~/.simlock/daemon.sock and says so.
  • simlock --gateway daemon start starts a gateway in ~/.simlock/gateway while the worker in ~/.simlock keeps running, and simlock status / simlock --gateway status report worker / gateway.
  • A gateway becomes present only through simlock --gateway daemon start or simlock --gateway config set; with no gateway present, any other --gateway command exits 2 with GATEWAY_NOT_PRESENT, and a --gateway mcp tool call fails with the same code.
  • On a machine that got its gateway first, simlock --local daemon start starts a worker, and plain commands reach it afterwards.
  • With defaults, a worker's HTTP listens on 4701 and a gateway's on 4700.
  • A config that sets mode is rejected at load and by config set, with a message that says what replaces it.
  • On a gateway-only machine, plain simlock daemon start and simlock status reach the gateway; no worker starts.
  • With both, a gateway-only command without --gateway fails with the worker's refusal plus the --gateway hint.
  • simlock --gateway mcp serves the gateway; plain simlock mcp follows the same folder choice as the CLI.
  • The user docs no longer mention SIMLOCK_HOME, the mode key, or several instances on one machine.

Open questions

Decisions

Shared values

Value or state Rule Written by Read by Source
--gateway A bare global flag, anywhere in the argv, like --token. No value; --gateway=<x> is a usage error. It routes the command to <root>/gateway. #708 #708, #709 ADR 0034 §3; src/cli/index.ts:40-61
--local The mirror of --gateway, named so it cannot clash with component install --worker <id>: routes to <root>, auto-starts a worker. --local=<x> and --local with --gateway are usage errors. #709 #709 ADR 0034 §3; src/cli/index.ts:40-61
Gateway folder join(<root>, "gateway"). Every gateway file (config.json, daemon.sock, daemon.log, instance.json, tokens.json, admin.token, workers.json, and the event history: events.jsonl, or history.db once #629 lands) lives there. #708 #708, #709 ADR 0034 §2
Root resolveSimlockHome(): $SIMLOCK_HOME, else ~/.simlock. Unchanged. — #708, #709 src/ports/paths.ts:17-20
Role → folder CLI and MCP only: async resolveDaemonHome({ root, flag, filesystem }) in src/ports/paths.ts returns { role, dataDirectory, gatewayPresent }. #708: gateway: boolean input, the flag alone decides the role. #709: the input becomes flag: "gateway" | "local" | undefined; with no flag the role is gateway when gatewayPresent and <root>/instance.json does not exist. The daemon uses the pure daemonDataDirectory(root, role) and never stats the other role's folder. #708 (creates), #709 (extends) CLI, MCP; daemon uses daemonDataDirectory ADR 0034 §2, §3
Daemon role at start The launcher passes --gateway to daemon/main.js for a gateway and sets SIMLOCK_HOME=<root> for both roles (never the gateway folder); the entry starts startDaemon({ role: "gateway" }). StartDaemonOptions.role defaults to "worker". #708 #708, #709 src/ports/daemon-launcher.ts:45-51; src/daemon/main.ts:1442
loadConfig role loadConfig({ role }), default "worker". config.mode equals role. A mode key in the file or the overrides throws ConfigError with the message below. #708 daemon, CLI src/core/config.ts:381-430
mode rejection message "mode" is no longer a config key. A daemon started with --gateway is a gateway; any other is a worker. Remove "mode" from config.json. #708 #708 tests, docs this spec
Present gateway exists(<root>/gateway/instance.json) || exists(<root>/gateway/config.json). ENOENT and ENOTDIR count as absent; any other error propagates. #708 #708, #709 ADR 0034 §3; src/ports/filesystem.ts:232-242
No-gateway refusal With --gateway, no gateway present, a command other than daemon start, config set or mcp, and no --help/-h: SimlockError("GATEWAY_NOT_PRESENT", "domain", "No gateway on this machine. Start one with: simlock --gateway daemon start") before the command runs; MCP runs resolveDaemonHome on every connect and throws the same error from a tool call's connect while no gateway is present. New code in src/contract/errors.ts: exit 2, HTTP 404, no details; raised only by the CLI and MCP, so no protocol bump. #708 #708, #709 ADR 0034 §3
CommandContext role flag #708: gateway: boolean. #709: replaced by flag: "gateway" | "local" | undefined. #708, #709 CLI commands, mcp src/cli/tree.ts:10
MCP launcher argument McpStdioLauncher takes { gateway: boolean } (#708), then the flag (#709). #708, #709 src/cli/commands/mcp.ts, src/mcp/main.ts src/cli/types.ts:27
e2e harness role WithDaemonOptions.role?: "gateway"; TestEnv.dataDirectory is home for a worker, <home>/gateway for a gateway; a gateway env's CLI spawns put --gateway first. #708 #708, #709 tests e2e/helpers/env.ts
HTTP default port Gateway role 4700, worker role 4701 (http.port still overrides). #708 daemon, docs src/core/config.ts:764
Worker marker <root>/instance.json exists. Written by the worker's loadInstanceId (unchanged). existing #709 src/daemon/main.ts:244-249
Hint text Shown when the chosen role is worker, --gateway was not given, a gateway is present, and the worker replied UNSUPPORTED_IN_WORKER_MODE. The worker's message, then . , then: A gateway also runs on this machine: run the command again with --gateway. Code and exit code unchanged (UNSUPPORTED_IN_WORKER_MODE, 2). #709 #709 tests, docs src/contract/errors.ts:357-362
daemon.mode on status.get, mode on config.get Unchanged on the wire; the value is the role. No protocol bump. — CLI passthrough, console, HTTP src/daemon/dispatcher.ts:358; src/contract/schemas.ts:838-842

Tasks

flowchart LR
  T1["#708 The folder sets the role"] --> T2["#709 Commands find the daemon by folder"]
Loading

Completion conditions → tasks:

Open work in the same area:

Written by an agent.

Activity

  1. added
    feature:specSpec in progress, business and technical together.
    on Oct 19, 2026
  2. V3RON commented on Oct 19, 2026

    @V3RON
    ContributorAuthor

    Spec updated: every section filled in (how it works, examples, completion conditions, decisions, shared values, tasks); open questions answered; tasks #708 and #709 added; ADR 0033 drafted in #702.

    Written by an agent.

  3. V3RON commented on Oct 19, 2026

    @V3RON
    ContributorAuthor

    Spec updated after the first spec check: --worker flag, GATEWAY_NOT_PRESENT for every --gateway command but daemon start/config set, worker HTTP default 4701, #708 now depends on #684, #629 and #630, ADR renumbered to 0034. Shared values, examples, completion conditions and open work changed.

    Written by an agent.

  4. V3RON commented on Oct 19, 2026

    @V3RON
    ContributorAuthor

    Spec updated after the second spec check: the worker flag is --local (no clash with component install --worker), #708 also depends on #631–#634, #712 to follow #708, MCP re-checks per call, harness and console-lane fixes, help before the no-gateway refusal.

    Written by an agent.

  5. added
    feature:plannedSplit into tasks. Never picked up itself.
    and removed
    feature:specSpec in progress, business and technical together.
    on Oct 19, 2026
  6. github-actions commented on Oct 20, 2026

    @github-actions

    All 2 sub-issues are closed. If the completion conditions hold on main, close this feature and flip its ADRs to Accepted.

  7. github-actions commented on Oct 20, 2026

    @github-actions

    All 2 sub-issues are closed. If the completion conditions hold on main, close this feature and flip its ADRs to Accepted.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature:plannedSplit into tasks. Never picked up itself.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions