loopctl-mcp-server 2.92.1 → 2.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -471,10 +471,10 @@ Enroll the dev machines that run the agent delivery loop, and see which are conn
471
471
 
472
472
  | Tool | Description |
473
473
  |---|---|
474
- | `runner_enroll` | Enroll this machine as a runner (`POST /api/v1/runners`). Required: `name` (the machine name the runner declares when it joins) and `token_file` (absolute, or starting with `~/`). The credential is written to `token_file` with mode 0600 and is **never returned**: the result is only `{ runner: {id, name, inserted_at}, token_file }`, because a tool result lands in the transcript and the token lets its holder join as that machine. The file is created exclusively, so an existing path (or a symlink there) is refused before anything is enrolled; missing parent directories are created 0700. If the token cannot be written after enrollment, the runner is revoked before the error returns. Only a 4xx is treated as a refusal: any other failure (timeout, 5xx, a 2xx that did not parse) may have enrolled the runner, so its response body is never echoed, the runner is revoked when the response proves its id, and otherwise the error points at `runner_list` and `runner_revoke`. 422 (name malformed or taken) and 403 (`custody_tier_required`, `api_key_mint_forbidden`) pass through with their code. |
474
+ | `runner_enroll` | Enroll this machine as a runner (`POST /api/v1/runners`). Required: `name` (the machine name the runner declares when it joins) and `token_file` (absolute, or starting with `~/`). Optional: `max_sessions` (1..64, default 2) — how many dispatches loopctl keeps in flight on this machine at once; the tenant's total across its runners is capped separately by the server. The credential is written to `token_file` with mode 0600 and is **never returned**: the result is only `{ runner: {id, name, inserted_at}, token_file }`, because a tool result lands in the transcript and the token lets its holder join as that machine. The file is created exclusively, so an existing path (or a symlink there) is refused before anything is enrolled; missing parent directories are created 0700. If the token cannot be written after enrollment, the runner is revoked before the error returns. Only a 4xx is treated as a refusal: any other failure (timeout, 5xx, a 2xx that did not parse) may have enrolled the runner, so its response body is never echoed, the runner is revoked when the response proves its id, and otherwise the error points at `runner_list` and `runner_revoke`. 422 (name malformed or taken) and 403 (`custody_tier_required`, `api_key_mint_forbidden`) pass through with their code. |
475
475
  | `runner_list` | List enrolled runners (`GET /api/v1/runners`). Optional: `include_revoked`. Enrollment only; connection state is `runner_pool`. |
476
476
  | `runner_revoke` | Revoke a runner (`DELETE /api/v1/runners/:id`): its credential stops authenticating and its live socket is disconnected. The undo for `runner_enroll`. Idempotent. Required: `id`. |
477
- | `runner_pool` | The tenant's connected runners from Presence (`GET /api/v1/runners/pool`): per machine name, `runner_id`, `joined_at`, `in_flight`, `draining`, `max_sessions`, the latest `sample`, `live_sockets` (above 1 means more than one process holds the credential), and the `node` and `machine_id` (Fly Machine) holding the socket. Presence converges only within a cluster. |
477
+ | `runner_pool` | The tenant's connected runners from Presence (`GET /api/v1/runners/pool`): per machine name, `runner_id`, `joined_at`, `draining`, the latest `sample`, `live_sockets` (above 1 means more than one process holds the credential), and the `node` and `machine_id` (Fly Machine) holding the socket. `in_flight` and `max_sessions` are the capacity loopctl holds in Postgres — what dispatch reserves against — and are null only for a runner revoked while its socket drains; `reported_in_flight` and `reported_max_sessions` are the runner's own last report, a hint. Presence converges only within a cluster. |
478
478
 
479
479
  ### Dispatch & Chain of Custody (v2) Tools
480
480
 
package/index.js CHANGED
@@ -7492,6 +7492,14 @@ const TOOLS = [
7492
7492
  description:
7493
7493
  "Absolute path, or one starting with ~/, for the token file. Must not exist yet.",
7494
7494
  },
7495
+ max_sessions: {
7496
+ type: "integer",
7497
+ minimum: 1,
7498
+ maximum: 64,
7499
+ description:
7500
+ "How many dispatches loopctl keeps in flight on this machine at once (default 2). " +
7501
+ "The tenant's total across all its runners is capped separately by the server.",
7502
+ },
7495
7503
  },
7496
7504
  required: ["name", "token_file"],
7497
7505
  },
@@ -7530,8 +7538,11 @@ const TOOLS = [
7530
7538
  name: "runner_pool",
7531
7539
  description:
7532
7540
  "The tenant's CONNECTED runners, read from Presence (GET /api/v1/runners/pool): per machine " +
7533
- "name, runner_id, joined_at, in_flight, draining, max_sessions, the latest health sample, " +
7534
- "live_sockets, and the node and machine_id (Fly Machine) holding the socket. live_sockets " +
7541
+ "name, runner_id, joined_at, draining, the latest health sample, live_sockets, and the node " +
7542
+ "and machine_id (Fly Machine) holding the socket. in_flight and max_sessions are the " +
7543
+ "CAPACITY loopctl holds in Postgres — the slots dispatch reserves against — and are null " +
7544
+ "only for a runner revoked while its socket drains; reported_in_flight and " +
7545
+ "reported_max_sessions are what the runner itself last reported, a hint. live_sockets " +
7535
7546
  "above 1 means more than one process holds that runner's credential. A killed runner " +
7536
7547
  "disappears once its socket closes. Presence converges only " +
7537
7548
  "within a cluster, so on an unclustered multi-node deployment a runner on another node is " +
package/lib/runners.js CHANGED
@@ -65,12 +65,18 @@ export function expandHome(p, homedir = os.homedir()) {
65
65
  }
66
66
 
67
67
  /**
68
- * `POST /api/v1/runners {name}`, with the returned token written to `token_file` and
69
- * never returned. Resolves to `{ runner: {id, name, inserted_at}, token_file }` or an
68
+ * `POST /api/v1/runners {name, max_sessions?}`, with the returned token written to
69
+ * `token_file` and never returned. Resolves to
70
+ * `{ runner: {id, name, max_sessions, inserted_at}, token_file }` or an
70
71
  * `{ error: true, status, body }` shape. It never throws.
72
+ *
73
+ * `max_sessions` is how many dispatches loopctl will keep in flight on this machine at
74
+ * once (loopctl #803). It is sent ONLY when given, so the server's own default applies
75
+ * otherwise, and the server refuses one out of range with a 422 that passes straight
76
+ * through — this does not second-guess the bound.
71
77
  */
72
78
  export async function enrollRunner(
73
- { name, token_file } = {},
79
+ { name, token_file, max_sessions } = {},
74
80
  { userKey, apiCall, fs = defaultFs, homedir = os.homedir() } = {},
75
81
  ) {
76
82
  if (!userKey) return refuse(MISSING_USER_KEY);
@@ -115,7 +121,8 @@ export async function enrollRunner(
115
121
 
116
122
  let result;
117
123
  try {
118
- result = await apiCall("POST", RUNNERS_PATH, { name });
124
+ const body = max_sessions === undefined ? { name } : { name, max_sessions };
125
+ result = await apiCall("POST", RUNNERS_PATH, body);
119
126
  } catch {
120
127
  result = { error: true, status: 0, body: "Enrollment request failed." };
121
128
  }
@@ -153,8 +160,15 @@ export async function enrollRunner(
153
160
  );
154
161
  }
155
162
 
163
+ // `max_sessions` only when the server sent one, so a server without loopctl #803 returns
164
+ // exactly the shape it always did rather than a key that is always undefined.
156
165
  return {
157
- runner: { id: runner.id, name: runner.name, inserted_at: runner.inserted_at },
166
+ runner: {
167
+ id: runner.id,
168
+ name: runner.name,
169
+ ...(runner.max_sessions === undefined ? {} : { max_sessions: runner.max_sessions }),
170
+ inserted_at: runner.inserted_at,
171
+ },
158
172
  token_file: tokenPath,
159
173
  };
160
174
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.92.1",
3
+ "version": "2.93.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",