Skip to content
Open
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
4 changes: 3 additions & 1 deletion docs/ABOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ Simlock is a CLI-first control plane (backed by a local daemon) that is the
same for both platforms and gives agents one primitive: **lease a device**.
An optional local stdio MCP integration exposes the focused lease/release
workflow to compatible agent clients; the CLI remains the full operator
interface.
interface. An optional, token-authenticated HTTP API lets remote agents lease
devices from a self-hosted simlock host over the network (see
[HTTP-API.md](HTTP-API.md)).

- `simlock lease` returns a *ready* device — booted and health-checked — that
no other agent will touch for the duration of the lease.
Expand Down
22 changes: 18 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
agent ──spawns──> simlock CLI ──┐
├─ shared daemon client ──unix socket──> simlock daemon
MCP client ──spawns──> stdio MCP ┘ │
remote agent ──token auth──> HTTP gateway ──same role interfaces────────┤
┌───┼─────────────┐
│ core (platform-│
│ agnostic) │
Expand All @@ -26,10 +28,22 @@ MCP client ──spawns──> stdio MCP ┘
emulator/adb)
```

- **CLI and stdio MCP server**: sibling thin frontends over the shared daemon
client and unix socket. The core never knows which frontend made a request.
The CLI is the full operator interface; the MCP server intentionally limits
its tool surface to leasing and releasing for an agent session.
- **CLI, stdio MCP server, and HTTP gateway**: sibling thin frontends. The
core never knows which frontend made a request. The CLI and MCP server sit
over the shared daemon client and unix socket; the CLI is the full operator
interface, and the MCP server intentionally limits its tool surface to
leasing and releasing for an agent session. The HTTP gateway is different in
kind, not just transport: it is the one frontend meant to be reached over a
real network, so it calls the same role interfaces (`LeaseCommands`,
`QueueControl`, `CapacityReader`, `CatalogReader`) in-process rather than
going through the unix socket, requires a bearer token on every route but
`GET /v1/healthz`, and only ever grants detached-style, TTL-renewed leases —
"held lease = live connection" does not survive a real network the way it
does a local process. It starts only after the daemon's own startup
convergence completes (see "Startup: claim first, converge after" below),
so unlike the socket protocol's parked-request behavior during that window,
a request arriving before then is simply refused. See
[HTTP-API.md](HTTP-API.md) for the full route reference.
- **CLI**: in the default *held* mode it acquires a lease, prints one JSON
result line on stdout, then stays alive holding the daemon connection; the
connection is the lease heartbeat. Progress streams as JSON lines on stderr.
Expand Down
21 changes: 21 additions & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,27 @@ under `capacity.config` — see
is running, both a global and a per-platform running limit must have room
before Simlock provisions or boots a shutdown device.

## `simlock token create --role <agent|operator> [--label <text>]` / `list` / `revoke <token-id>`

Mint and manage bearer tokens for the HTTP API. Operates on
`~/.simlock/tokens.json` directly (under `SIMLOCK_HOME`) — no daemon
round-trip, like `config` reading its file.

`create` prints the minted secret **once**, alongside the token record:

```json
{"token":{"id":"tok_9f2c","role":"agent","label":"ci-runner","createdAt":1735689600000},"secret":"slk_Wn9…"}
```

Only the secret's SHA-256 hash is ever persisted; there is no way to recover
a lost secret, only to `revoke` the token and `create` a new one. The token
id doubles as the requester identity over HTTP — one token is one requester,
same as the CLI's `--agent-id`.

`list` prints `{"tokens":[...]}` — the same record shape as `create`, minus
the secret and its hash. `revoke <token-id>` prints `{"revoked":true}`, or a
structured `UNKNOWN_TOKEN` error (exit 1) for an id that does not exist.

## Environment variables

### `SIMLOCK_HOME`
Expand Down
5 changes: 5 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ a warning. Inspect the effective, merged configuration at any time with
| `lease.heldTtlBackstopMs` | Backstop TTL for held-mode leases, in case the holding process dies without releasing. | `1 hour` |
| `lease.detachedTtlMs` | TTL for detached-mode leases before they must be renewed with `simlock lease renew`. | `15 minutes` |
| `lease.heartbeatIntervalMs` | How often the daemon pings a held-mode connection that declared the `heartbeat` capability; each pong slides that connection's leases' TTL back out to a full `heldTtlBackstopMs`. Must be `<= lease.heldTtlBackstopMs / 4`. | `5 minutes` |
| `http.enabled` | Master switch for the network-facing HTTP API (see [HTTP-API.md](HTTP-API.md)). Off by default; the daemon binds nothing until this is `true`. | `false` |
| `http.host` | Address the HTTP listener binds. `127.0.0.1` keeps it loopback-only; reaching it remotely is the operator's own tunnel (Tailscale, cloudflared, reverse proxy) — Simlock does no TLS termination in v1. | `127.0.0.1` |
| `http.port` | Port the HTTP listener binds. Must be an integer `1`-`65535`. | `4700` |
| `diskPressure.freeBytesThreshold` | Free disk space below which Simlock treats the machine as under disk pressure. | `10 GiB` |
| `eventBuffer.capacity` | Number of business events kept in the in-memory ring buffer (see `simlock events`). | `1000` |
| `health.enabled` | Master switch for leased-device crash detection and recovery. | `true` |
Expand All @@ -36,6 +39,8 @@ must be non-negative numbers (milliseconds and bytes, respectively).
`health.maxConcurrentRecoveries` must be positive integers.
`stalledTransition.thresholdMultiplier` must be a number `>= 1`;
`stalledTransition.minimumThresholdMs` must be a non-negative number.
`http.enabled` is a boolean, `http.host` a string, and `http.port` an
integer in `1`-`65535`.
See [CLI.md](CLI.md#simlock-config-get-keyset-key-value) for the
`simlock config` command itself.

Expand Down
2 changes: 1 addition & 1 deletion docs/EVENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ in short: `subject.past-tense-fact`, emitted post-commit, facts not commands.
| `lease.renewed` | lease id, new deadline | an explicit `simlock lease renew` succeeded (either mode), **or** a held-mode connection that declared the `heartbeat` capability answered a `lease.heartbeat` push (fires once per lease per `lease.heartbeatIntervalMs` while the holder stays alive) | LeaseLifecycle | implemented |
| `lease.released` | lease id, device id, reason (closed/explicit/killed/orphaned/device-lost) | holder connection closed, explicit release, (orphaned) a `held` lease found still persisted at daemon startup, which cannot have a live holder across a restart, or (device-lost) a leased device could not be recovered after it stopped running outside simlock | LeaseLifecycle | implemented |
| `lease.expired` | lease id, device id | TTL backstop fired without a heartbeat sliding it first — for a capability-declaring holder this means it stopped ponging (crashed, hung, or lost its socket); for one that never declared the capability it means the grant-time TTL (or the last explicit `simlock lease renew`) simply ran out, exactly as before this change | LeaseLifecycle | implemented |
| `lease.rejected` | request spec, reason (timeout/no-wait/unresolvable-spec/already-leased/boot-timeout/killed) | a request ended without a grant | LeaseAcquisitionCoordinator / WaitQueue | implemented |
| `lease.rejected` | request spec, reason (timeout/no-wait/unresolvable-spec/already-leased/boot-timeout/killed/cancelled) | a request ended without a grant; `cancelled` is an explicit single-request cancel (`LeaseEngine#cancelPending`, backing `DELETE /v1/lease-requests/{id}`) of a still-queued waiter -- one with device work already in flight is reported `not-cancellable` instead, the same envelope the queue timeout already uses | LeaseAcquisitionCoordinator / WaitQueue | implemented |

## Device lifecycle

Expand Down
Loading