Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .fallowrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
"maxCrap": 100,
"reason": "Glue scripts for the delivery pipeline: git and transcript walks, option parsing, a Markdown shape check. They have no coverage pipeline, so CRAP is complexity squared; each is run against real PRs and transcripts when it changes, and none holds product logic."
},
{
"files": ["src/agent-device/host/agent-device-supervisor.test.ts"],
"maxCrap": 80,
"reason": "The supervisor's test arranges a process table, a clock, a filesystem and a scripted runner in one helper so each of its cases reads as one claim; the helper is branching setup, not logic under test."
},
{
"files": ["e2e/fake-driver/fake-driver.ts"],
"maxCrap": 45,
Expand Down
121 changes: 121 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"regex": "(?:^|/)android/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "drivers/android is entered through its index.js"
},
{
"regex": "(?:^|/)agent-device/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "agent-device is entered through its index.js"
},
{
"regex": "(?:^|/)(?:testing|test-[^/]*)\\.js$",
"message": "only a *.test.ts file or a test-*.ts helper imports a testing.js or test-*.js file"
Expand Down Expand Up @@ -74,6 +78,10 @@
],
"message": "core and leasing reach the filesystem and processes only through injected ports"
},
{
"regex": "(?:^|/)agent-device/",
"message": "the core, leasing and the drivers import nothing from the agent-device edge"
},
{
"regex": "(?:^|/)(?:testing|test-[^/]*)\\.js$",
"message": "only a *.test.ts file or a test-*.ts helper imports a testing.js or test-*.js file"
Expand Down Expand Up @@ -127,6 +135,10 @@
],
"message": "core and leasing reach the filesystem and processes only through injected ports"
},
{
"regex": "(?:^|/)agent-device/",
"message": "the core, leasing and the drivers import nothing from the agent-device edge"
},
{
"regex": "(?:^|/)(?:testing|test-[^/]*)\\.js$",
"message": "only a *.test.ts file or a test-*.ts helper imports a testing.js or test-*.js file"
Expand Down Expand Up @@ -202,6 +214,10 @@
"regex": "(?:^|/)daemon/(?:$|[^d]|d(?:$|[^i]|i(?:$|[^s]|s(?:$|[^p]|p(?:$|[^a]|a(?:$|[^t]|t(?:$|[^c]|c(?:$|[^h]|h(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.)))))))))))",
"message": "the gateway imports only the daemon's dispatch contract"
},
{
"regex": "(?:^|/)agent-device/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "agent-device is entered through its index.js"
},
{
"regex": "(?:^|/)(?:testing|test-[^/]*)\\.js$",
"message": "only a *.test.ts file or a test-*.ts helper imports a testing.js or test-*.js file"
Expand Down Expand Up @@ -239,6 +255,10 @@
"regex": "(?:^|/)gateway/",
"message": "only the composition root, main.ts, knows a gateway exists"
},
{
"regex": "(?:^|/)agent-device/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "agent-device is entered through its index.js"
},
{
"regex": "(?:^|/)(?:testing|test-[^/]*)\\.js$",
"message": "only a *.test.ts file or a test-*.ts helper imports a testing.js or test-*.js file"
Expand Down Expand Up @@ -268,6 +288,103 @@
]
}
},
{
"files": ["src/drivers/**"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"regex": "(?:^|/)core/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "core is entered through its index.js"
},
{
"regex": "(?:^|/)leasing/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "leasing is entered through its index.js"
},
{
"regex": "(?:^|/)gateway/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "gateway is entered through its index.js"
},
{
"regex": "(?:^|/)ios/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "drivers/ios is entered through its index.js"
},
{
"regex": "(?:^|/)android/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "drivers/android is entered through its index.js"
},
{
"regex": "(?:^|/)agent-device/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "agent-device is entered through its index.js"
},
{
"regex": "(?:^|/)(?:testing|test-[^/]*)\\.js$",
"message": "only a *.test.ts file or a test-*.ts helper imports a testing.js or test-*.js file"
},
{
"regex": "(?:^|/)agent-device/",
"message": "the core, leasing and the drivers import nothing from the agent-device edge"
}
]
}
]
}
},
{
"files": ["src/agent-device/**"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"regex": "(?:^|/)core/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "core is entered through its index.js"
},
{
"regex": "(?:^|/)leasing/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "leasing is entered through its index.js"
},
{
"regex": "(?:^|/)gateway/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "gateway is entered through its index.js"
},
{
"regex": "(?:^|/)ios/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "drivers/ios is entered through its index.js"
},
{
"regex": "(?:^|/)android/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "drivers/android is entered through its index.js"
},
{
"regex": "(?:^|/)agent-device/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "agent-device is entered through its index.js"
},
{
"regex": "(?:^|/)(?:testing|test-[^/]*)\\.js$",
"message": "only a *.test.ts file or a test-*.ts helper imports a testing.js or test-*.js file"
},
{
"group": [
"fs",
"fs/**",
"node:fs",
"node:fs/**",
"child_process",
"child_process/**",
"node:child_process",
"node:child_process/**"
],
"message": "agent-device reaches the filesystem and processes only through injected ports"
}
]
}
]
}
},
{
"files": ["**/*.test.ts", "**/test-*.ts"],
"rules": {
Expand Down Expand Up @@ -298,6 +415,10 @@
{
"regex": "(?:^|/)android/(?:$|[^i]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))",
"message": "drivers/android is entered through its index.js"
},
{
"regex": "(?:^|/)agent-device/(?:$|[^it]|i(?:$|[^n]|n(?:$|[^d]|d(?:$|[^e]|e(?:$|[^x]|x(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.)))))))|t(?:$|[^e]|e(?:$|[^s]|s(?:$|[^t]|t(?:$|[^i]|i(?:$|[^n]|n(?:$|[^g]|g(?:$|[^\\.]|\\.(?:$|[^j]|j(?:$|[^s]|s.))))))))))",
"message": "agent-device is entered through its index.js, or testing.js from a test file"
}
]
}
Expand Down
2 changes: 2 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ edit yourself keeps the mode you give it until the next `config set`; run
| `gateway.url` | **Worker side.** Base URL of the gateway this worker joins, e.g. `wss://gw.example:4700`; the worker dials `<url>/v1/uplink` and upgrades it to a WebSocket. Joining grants that gateway the `admin` role on this daemon, and `gateway.token` rides on the upgrade request as a bearer credential, so use `wss://` — or plain `ws://` only over loopback or inside your own tunnel. `http://` and `https://` are rejected at load. Unset means "do not join a fleet": the default, and the only thing that changes about a joined worker. | unset |
| `gateway.token` | **Worker side.** The join token (`simlock token create --role worker`, minted on the gateway) this worker presents when it opens its uplink. Required whenever `gateway.url` is set. `simlock config` shows it; the daemon log and the `daemon.started` event show `[redacted]`. It is stored in plain text in `config.json`, which `simlock config set` writes owner-only. | unset |
| `gateway.label` | **Worker side.** Display name for this worker in `simlock worker list`, `status`, the console, and on the lease's `worker` block. Display-only: nothing routes on it and it need not be unique. | the worker's own id |
| `agentDevice.enabled` | **Worker side.** Run one agent-device daemon on this worker, started and watched by Simlock. It sees Simlock's devices and no others, refuses `boot` and `shutdown`, and keeps its files in `~/.simlock/agent-device`. If agent-device is missing, older than 0.21.25, speaks another RPC protocol or needs a newer Node than Simlock runs on, it is not started and Simlock runs on without it. A daemon a killed Simlock left behind is stopped at the next start. | `false` |
| `agentDevice.command` | **Worker side.** The agent-device executable: a name looked up on `PATH`, or a path. Used only when `agentDevice.enabled` is `true`. | `agent-device` |
| `exec.timeoutMs` | **Worker side.** How long one `device.exec` command (`simlock simctl` / `simlock adb` against a gateway or over HTTP) may run before the worker kills it and the operation fails with `EXEC_TIMEOUT`. Authoritative: it bounds the process that actually runs. | `10 minutes` |
| `gateway.routing` | **Gateway side.** Which routing policy places a queued request on a worker. `warm-then-free` is the only policy in v1. Among the workers that can serve the request, in order: a worker whose health is not `running`, or that has requests of its own waiting, is left out; a worker holding a ready device that fits goes first (the device's mode must fit the request, a request with no mode fits a device that serves the worker's default mode, and its runtime must be the one requested or, with none requested, the one the worker would pick: its default runtime if the model pairs with it, otherwise the only runtime the model pairs with); otherwise a worker with no free running slot for the platform or overall is left out (a running device that is not leased counts as free: the worker shuts it down to make room), a worker under its RAM budget for the platform is preferred over one at it (a worker at its budget is still asked when it is the only one left), and the one with the most free running capacity for the platform wins. A worker that is left out is busy, not unable: the request waits for it. Ties go to the lowest worker id. | `warm-then-free` |
| `gateway.disconnectedRetentionMs` | **Gateway side.** How long a disconnected worker is kept (greyed, never dispatched to) before the gateway forgets it. The clock is held while the gateway still knows of gateway-issued leases on that worker, and that hold ends when the last of those leases passes its deadline. | `24 hours` |
Expand Down
10 changes: 10 additions & 0 deletions docs/internal/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ modules with.
| `LeaseHealthMonitor` | `src/leasing/lease-health-monitor.ts` | Observing leased devices on a tick, rebooting a crashed one under its lease, giving the lease up as `device-lost` when recovery is exhausted. | Touch an unleased device. |
| `PhysicalDeviceWatcher` | `src/leasing/physical-device-watcher.ts` | The presence tick (every `health.probeIntervalMs`, whatever `health.enabled` says), the grace-period count of not-present reads, `absent` and its reason, the device-lost ending of a lease on a device that is gone, and the return reclaim of a device that is back (ADR 0022 §7). | Retry quarantine, grant, or run a driver verb but the presence listing on a leased device. |

## Agent-device edge

`src/agent-device/` holds the agent-device daemon a worker runs beside its drivers (ADR 0023). It is entered only through `src/agent-device/index.ts`; the core, leasing and the drivers import nothing from it, and its non-test files import no `fs` or `child_process`.

| Component | Where | Owns | Does not |
|---|---|---|---|
| `AgentDeviceSupervisor` | `src/agent-device/host/agent-device-supervisor.ts` | Starting, watching and stopping the one agent-device daemon: reaping one a killed simlock left (by a pid record and an identity check), the private state dir and policy file, the daemon's environment, readiness through `daemon.json`, the port and `/health`, restart on death, and the availability state (`off`, `starting`, `running`, `unavailable` with a reason and remedy). | Serve agent-device traffic; report to `simlock doctor`; decide which devices exist (the drivers' environments do). |
| Install lookup, compatibility, daemon environment | `src/agent-device/host/install.ts`, `src/agent-device/shared/` | Finding the agent-device install and its daemon entry; the minimum versions and RPC protocol; the environment and policy data. | Run a process. |
| `AgentDeviceHealthProbe` | `src/agent-device/host/health.ts` | The `GET /health` read, with a time limit. | Judge the answer. |

## Drivers

| Component | Where | Owns | Does not |
Expand Down
4 changes: 4 additions & 0 deletions e2e/fake-driver/fake-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,10 @@ export class OutOfProcessFakeDriver implements Driver {
return this.#lastKnownEstimateMs?.[estimate.operation] ?? 0;
}

virtualLeaseEnvironment(): Readonly<Record<string, string>> {
return {};
}

leaseEnvironment(
device: VirtualDriverDevice | PhysicalDriverDevice,
): Readonly<Record<string, string>> {
Expand Down
Loading
Loading