@stage-labs/metro 0.1.0-beta.50 → 0.1.0-beta.52

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.
Files changed (51) hide show
  1. package/dist/api.js +8 -142
  2. package/dist/cli.js +27 -131
  3. package/dist/control.js +5 -104
  4. package/dist/plugin.js +2 -4
  5. package/dist/runtime.js +18 -72
  6. package/dist/serve.js +3 -2
  7. package/dist/tail.js +7 -8
  8. package/dist/update.js +13 -1
  9. package/package.json +1 -1
  10. package/runtime/node_modules/@metro-labs/mcp/package.json +1 -2
  11. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +3 -35
  12. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +7 -22
  13. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +4 -54
  14. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +1 -2
  15. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +29 -208
  16. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +0 -4
  17. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +2 -37
  18. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +0 -3
  19. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +4 -6
  20. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +3 -14
  21. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +0 -81
  22. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +2 -2
  23. package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +119 -0
  24. package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +42 -12
  25. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +1 -73
  26. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +1 -172
  27. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +0 -5
  28. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +2 -89
  29. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +3 -262
  30. package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +1 -1
  31. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +2 -140
  32. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +1 -89
  33. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +14 -41
  34. package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +5 -5
  35. package/runtime/node_modules/@metro-labs/whatsapp/package.json +0 -2
  36. package/runtime/runtime.json +1 -1
  37. package/dist/prompt.js +0 -61
  38. package/dist/store.js +0 -93
  39. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +0 -76
  40. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +0 -20
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +0 -141
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +0 -68
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +0 -58
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +0 -214
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +0 -139
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +0 -105
  47. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +0 -143
  48. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +0 -309
  49. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +0 -24
  50. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +0 -105
  51. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +0 -18
package/dist/tail.js CHANGED
@@ -1,5 +1,5 @@
1
- import { runConfigKey } from './api.js';
2
- import { assertAgentId, localUrl, readRunToken } from './runtime.js';
1
+ import { localAgents } from './local.js';
2
+ import { assertAgentId, localUrl } from './runtime.js';
3
3
  const BACKOFF_MS = [1_000, 2_000, 5_000];
4
4
  const STALL_MS = 90_000;
5
5
  export const backoffAt = (attempt) => BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)] ?? 5_000;
@@ -22,11 +22,10 @@ function keySource(agentId) {
22
22
  return { resolve: () => Promise.resolve(fromEnv), fixed: true };
23
23
  return {
24
24
  resolve: () => {
25
- const token = readRunToken(agentId);
26
- if (token === null)
27
- throw new Error(`this machine is not authorized for ${agentId} — ` +
28
- `run 'metro start ${agentId}' once, or set METRO_AGENT_KEY`);
29
- return runConfigKey(token);
25
+ const agent = localAgents().find((a) => a.id === agentId);
26
+ if (agent === undefined)
27
+ throw new Error(`no agent ${agentId} on this machine — create or restore it in the web UI of metro serve, or set METRO_AGENT_KEY`);
28
+ return Promise.resolve(agent.key);
30
29
  },
31
30
  fixed: false,
32
31
  };
@@ -98,7 +97,7 @@ async function settle(end, source, key, agentId, state) {
98
97
  return source.resolve();
99
98
  }
100
99
  if (end === 'unreachable')
101
- note(state, `no metro daemon on ${localUrl()} — run 'metro start ${agentId}'`);
100
+ note(state, `no metro daemon on ${localUrl()} — run 'metro serve' (agent ${agentId})`);
102
101
  else
103
102
  note(state, 'stream ended — reconnecting');
104
103
  return key;
package/dist/update.js CHANGED
@@ -9,7 +9,19 @@ export function installArgs(manager, version) {
9
9
  ? ['add', '--global', spec]
10
10
  : ['install', '--global', spec];
11
11
  }
12
- export async function update() {
12
+ export const checkReport = (current, latest) => ({
13
+ current,
14
+ latest,
15
+ newer: latest !== '' && isNewer(latest, current),
16
+ });
17
+ export async function updateCheck() {
18
+ return checkReport(currentVersion(), await publishedVersion());
19
+ }
20
+ export async function update(argv = []) {
21
+ if (argv.includes('--check')) {
22
+ process.stdout.write(`${JSON.stringify(await updateCheck())}\n`);
23
+ return 0;
24
+ }
13
25
  const current = currentVersion();
14
26
  const newest = await publishedVersion();
15
27
  if (newest === '') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.50",
3
+ "version": "0.1.0-beta.52",
4
4
  "description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,8 +27,7 @@
27
27
  "./stations/station-runtime": "./src/stations/station-runtime.ts",
28
28
  "./stations/account-store": "./src/stations/account-store.ts",
29
29
  "./stations/attachments": "./src/stations/attachments.ts",
30
- "./stations/messaging-normalize": "./src/stations/messaging-normalize.ts",
31
- "./db/whatsapp-login": "./src/db/whatsapp-login.ts"
30
+ "./stations/messaging-normalize": "./src/stations/messaging-normalize.ts"
32
31
  },
33
32
  "scripts": {
34
33
  "start": "bun src/server.ts",
@@ -1,7 +1,7 @@
1
1
  import { webhookPort } from './tunnel.js';
2
2
  import type { IncomingMessage, ServerResponse } from 'node:http';
3
+ import { parseId } from '../db/ids.js';
3
4
  import { errMsg, log } from './log.js';
4
- import { publicBaseOrDefault } from './attach-serve.js';
5
5
  import {
6
6
  apiFailure,
7
7
  apiSession,
@@ -20,7 +20,6 @@ import {
20
20
  type AccountRoute,
21
21
  } from './account-api.js';
22
22
  import {
23
- parseAgentId,
24
23
  type AgentSummary,
25
24
  type CreatedAgent,
26
25
  type DeletedAgent,
@@ -52,8 +51,6 @@ export interface AgentApiDeps extends AccountApiDeps {
52
51
  capabilities: () => Record<string, string[]>;
53
52
  attachable?: string[];
54
53
  liveness: () => Map<string, { connected: boolean; lastSeenAt: number }>;
55
- releaseRuntime: (subject: string, agentId: string) => Promise<void>;
56
- runtimes: (agentIds: string[]) => Promise<Map<string, string>>;
57
54
  connectorIds: (agentIds: string[]) => Promise<Map<string, string[]>>;
58
55
  }
59
56
 
@@ -61,7 +58,6 @@ type Routable =
61
58
  | { kind: 'collection' }
62
59
  | { kind: 'agent'; id: string }
63
60
  | { kind: 'key'; id: string }
64
- | { kind: 'runtime'; id: string }
65
61
  | { kind: 'accounts'; id: string; route: AccountRoute };
66
62
 
67
63
  type Target = Routable | { kind: 'unknown' } | null;
@@ -69,8 +65,6 @@ type Target = Routable | { kind: 'unknown' } | null;
69
65
  function subTarget(id: string, rest: string[]): Target {
70
66
  if (rest.length === 0) return { kind: 'agent', id };
71
67
  if (rest.length === 1 && rest[0] === 'key') return { kind: 'key', id };
72
- if (rest.length === 1 && rest[0] === 'runtime')
73
- return { kind: 'runtime', id };
74
68
  if (rest[0] !== 'accounts') return { kind: 'unknown' };
75
69
  const route = accountRoute(rest.slice(1));
76
70
  return route === null ? { kind: 'unknown' } : { kind: 'accounts', id, route };
@@ -82,14 +76,10 @@ export function target(path: string): Target {
82
76
  const segments = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
83
77
  const head = segments[0];
84
78
  if (head === undefined) return { kind: 'collection' };
85
- const id = parseAgentId(head);
79
+ const id = parseId(head);
86
80
  return id === null ? { kind: 'unknown' } : subTarget(id, segments.slice(1));
87
81
  }
88
82
 
89
- export function mcpEndpoint(): string {
90
- return `${publicBaseOrDefault()}/mcp`;
91
- }
92
-
93
83
  const localMcpEndpoint = (): string => `http://127.0.0.1:${String(webhookPort())}/mcp`;
94
84
 
95
85
  export function mcpAddCommand(key: string): string {
@@ -132,14 +122,12 @@ function livenessPayload(
132
122
  function agentPayload(
133
123
  agent: AgentSummary,
134
124
  live: Map<string, { connected: boolean; lastSeenAt: number }>,
135
- runtimes: Map<string, string>,
136
125
  connectors: Map<string, string[]>,
137
126
  ): Record<string, unknown> {
138
127
  return {
139
128
  id: agent.id,
140
129
  name: agent.name,
141
130
  owned: agent.owned,
142
- runtime: agent.owned ? (runtimes.get(agent.id) ?? null) : null,
143
131
  connector_ids: connectors.get(agent.id) ?? [],
144
132
  ...livenessPayload(agent, live),
145
133
  ...keyPayload(agent),
@@ -164,14 +152,9 @@ async function handleList(
164
152
  }
165
153
  const list = await deps.listAgents(session.subject, project);
166
154
  const live = deps.liveness();
167
- const held = await deps.runtimes(
168
- list.filter((a) => a.owned).map((a) => a.id),
169
- );
170
155
  const connectors = await deps.connectorIds(list.map((a) => a.id));
171
156
  const base = {
172
- subject: session.subject,
173
- endpoint: mcpEndpoint(),
174
- agents: list.map((a) => agentPayload(a, live, held, connectors)),
157
+ agents: list.map((a) => agentPayload(a, live, connectors)),
175
158
  capabilities: deps.capabilities(),
176
159
  attachable: deps.attachable ?? ATTACHABLE,
177
160
  };
@@ -185,18 +168,6 @@ async function handleList(
185
168
  sendJson(req, res, 200, { ...base, accounts, unavailable });
186
169
  }
187
170
 
188
- async function handleRuntime(
189
- req: IncomingMessage,
190
- res: ServerResponse,
191
- deps: AgentApiDeps,
192
- session: ApiSession,
193
- id: string,
194
- ): Promise<void> {
195
- await deps.releaseRuntime(session.subject, id);
196
- log.info({ agent: id }, 'agent-api: runtime released');
197
- sendJson(req, res, 200, { agent: id, runtime: null });
198
- }
199
-
200
171
  async function handleCreate(
201
172
  req: IncomingMessage,
202
173
  res: ServerResponse,
@@ -268,8 +239,6 @@ async function routeAgent(
268
239
  ): Promise<void> {
269
240
  try {
270
241
  if (tgt.kind === 'key') await handleResetKey(req, res, deps, session, tgt.id);
271
- else if (tgt.kind === 'runtime')
272
- await handleRuntime(req, res, deps, session, tgt.id);
273
242
  else if (tgt.kind === 'agent')
274
243
  await handleDelete(req, res, deps, session, tgt.id);
275
244
  else if (req.method === 'GET') await handleList(req, res, deps, session);
@@ -283,7 +252,6 @@ const ALLOWED: Record<AgentTarget['kind'], string[]> = {
283
252
  collection: ['GET', 'POST'],
284
253
  agent: ['DELETE'],
285
254
  key: ['POST'],
286
- runtime: ['DELETE'],
287
255
  };
288
256
 
289
257
  function methodAllowed(tgt: Routable, method: string | undefined): boolean {
@@ -10,10 +10,15 @@ import {
10
10
  type ApiSession,
11
11
  } from './api-http.js';
12
12
  import { parseId } from '../db/ids.js';
13
- import type { AgentConnectors } from '../db/agent-connectors.js';
14
13
 
15
14
  const PREFIX = '/api/agents';
16
15
 
16
+ export interface AgentConnectors {
17
+ id: string;
18
+ name: string;
19
+ connectorIds: string[];
20
+ }
21
+
17
22
  export interface AgentConnectorApiDeps {
18
23
  agentConnectors: (subject: string, agentId: string) => Promise<AgentConnectors>;
19
24
  addConnector: (
@@ -26,23 +31,17 @@ export interface AgentConnectorApiDeps {
26
31
  agentId: string,
27
32
  connectorId: string,
28
33
  ) => Promise<AgentConnectors>;
29
- mintCode: (
30
- subject: string,
31
- agentId: string,
32
- ) => Promise<{ code: string; expiresAt: number; agent: string }>;
33
34
  }
34
35
 
35
36
  type Routable =
36
37
  | { kind: 'connectors'; id: string }
37
- | { kind: 'connector'; id: string; connectorId: string }
38
- | { kind: 'code'; id: string };
38
+ | { kind: 'connector'; id: string; connectorId: string };
39
39
 
40
40
  type Target = Routable | { kind: 'unknown' } | null;
41
41
 
42
42
  const ALLOWED: Record<Routable['kind'], string[]> = {
43
43
  connectors: ['GET', 'POST'],
44
44
  connector: ['DELETE'],
45
- code: ['POST'],
46
45
  };
47
46
 
48
47
  function connectorTarget(id: string, rest: string[]): Target {
@@ -61,7 +60,6 @@ export function target(path: string): Target {
61
60
  const head = segments[1];
62
61
  if (id === null || head === undefined) return null;
63
62
  if (head === 'connectors') return connectorTarget(id, segments.slice(1));
64
- if (head === 'code' && segments.length === 2) return { kind: 'code', id };
65
63
  return null;
66
64
  }
67
65
 
@@ -84,18 +82,6 @@ async function handleConnectors(
84
82
  sendJson(req, res, 200, await deps.addConnector(session.subject, id, connectorId));
85
83
  }
86
84
 
87
- async function handleCode(
88
- req: IncomingMessage,
89
- res: ServerResponse,
90
- deps: AgentConnectorApiDeps,
91
- session: ApiSession,
92
- id: string,
93
- ): Promise<void> {
94
- const minted = await deps.mintCode(session.subject, id);
95
- log.info({ agent: id, subject: session.subject }, 'agent-api: minted pairing code');
96
- sendJson(req, res, 201, minted);
97
- }
98
-
99
85
  async function route(
100
86
  req: IncomingMessage,
101
87
  res: ServerResponse,
@@ -105,7 +91,6 @@ async function route(
105
91
  ): Promise<void> {
106
92
  try {
107
93
  if (tgt.kind === 'connectors') await handleConnectors(req, res, deps, session, tgt.id);
108
- else if (tgt.kind === 'code') await handleCode(req, res, deps, session, tgt.id);
109
94
  else
110
95
  sendJson(
111
96
  req,
@@ -1,13 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { ApiError } from './api-error.js';
3
3
  import { errMsg, log } from './log.js';
4
- import {
5
- verifyAgentToken,
6
- verifyRunToken,
7
- verifySession,
8
- type AgentClaims,
9
- type RunClaims,
10
- } from './session.js';
4
+ import { verifySession } from './session.js';
11
5
  import { extractToken } from '../mcp/request-identity.js';
12
6
 
13
7
  const BODY_MAX = 4 * 1024;
@@ -54,53 +48,9 @@ export function apiSession(req: IncomingMessage): ApiSession | null {
54
48
  }
55
49
  }
56
50
 
57
- export function runIdentity(req: IncomingMessage): RunClaims | null {
58
- const secret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
59
- if (secret === '') return null;
60
- const token = extractToken(req) ?? '';
61
- if (token === '') return null;
62
- try {
63
- const claims = verifyRunToken(token, secret);
64
- return { ...claims, subject: claims.subject.toLowerCase() };
65
- } catch {
66
- return null;
67
- }
68
- }
69
-
70
- export interface AgentIdentity extends AgentClaims {
71
- runtimeId?: string;
72
- }
73
-
74
- export function agentIdentity(req: IncomingMessage): AgentIdentity | null {
75
- const secret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
76
- if (secret === '') return null;
77
- const token = extractToken(req) ?? '';
78
- if (token === '') return null;
79
- try {
80
- const { subject, agentId } = verifyAgentToken(token, secret);
81
- return { subject: subject.toLowerCase(), agentId };
82
- } catch {
83
- const run = runIdentity(req);
84
- if (run === null) return null;
85
- return { subject: run.subject, agentId: run.agentId, runtimeId: run.runtimeId };
86
- }
87
- }
88
-
89
- export type Fence = (runtimeId: string, agentId: string) => Promise<void>;
90
-
91
- const RELEASED = 'this machine no longer holds the agent';
92
-
93
- export async function assertLease(
94
- who: AgentIdentity,
95
- fence: Fence,
96
- ): Promise<void> {
97
- if (who.runtimeId === undefined) return;
98
- try {
99
- await fence(who.runtimeId, who.agentId);
100
- } catch (err) {
101
- if (err instanceof ApiError) throw new ApiError(RELEASED, 401);
102
- throw err;
103
- }
51
+ export interface AgentIdentity {
52
+ subject: string;
53
+ agentId: string;
104
54
  }
105
55
 
106
56
  export function projectParam(req: IncomingMessage): string | null {
@@ -34,8 +34,7 @@ export function publicBaseUrl(): string | null {
34
34
  return currentTunnelUrl();
35
35
  }
36
36
 
37
- const servesLocally = (): boolean =>
38
- (process.env.METRO_RUN_TOKEN?.trim() ?? '') !== '' || isLocalMode();
37
+ const servesLocally = (): boolean => isLocalMode();
39
38
 
40
39
  const localBase = (): string | null =>
41
40
  servesLocally() ? `http://127.0.0.1:${String(webhookPort())}` : null;
@@ -1,4 +1,3 @@
1
- import type { ProjectApiDeps } from './project-api.js';
2
1
  import { join } from 'node:path';
3
2
  import { type Server } from 'node:http';
4
3
  import { userSelf } from './events.js';
@@ -19,19 +18,7 @@ import {
19
18
  startWebhookServer,
20
19
  trainEventToMetroEvent,
21
20
  } from './http.js';
22
- import { type RunApiDeps } from './run-api.js';
23
- import {
24
- claimRuntime,
25
- fenceRuntime,
26
- runtimeLabels,
27
- touchRuntime,
28
- } from '../db/runtimes.js';
29
- import {
30
- blockedStationsFor,
31
- mintAgentCodeForUser,
32
- releaseRuntimeForUser,
33
- } from '../db/runtime-admin.js';
34
- import { loadAgentForRuntime, localAgentKey } from '../db/materialize.js';
21
+ import { localAgentKey } from '../db/materialize.js';
35
22
  import { fileSource } from '../db/file-source.js';
36
23
  import { ensureLocalSessionSecret } from './local-secret.js';
37
24
  import { applyLocalOwner } from './local-owner.js';
@@ -51,60 +38,15 @@ import {
51
38
  stationByName,
52
39
  } from '../stations/registry.js';
53
40
  import { prepareAccount } from '../stations/attach.js';
41
+ import { materializeFrom, reloadFrom } from '../db/materialize.js';
54
42
  import {
55
- materializeFrom,
56
- materializeFromDb,
57
- reloadAccountsFromDb,
58
- reloadFrom,
59
- type ReloadedStations,
60
- } from '../db/materialize.js';
61
- import {
62
- httpSource,
63
- runtimeConfigFromEnv,
64
- startRuntimePoller,
65
- } from './runtime-source.js';
66
- import { createAgentForUser, deleteAgentForUser, listAgentsForUser, ownedAgentOrThrow, resetAgentKeyForUser, type ResetAgentKey,} from '../db/agent-admin.js';
67
- import {
68
- attachAccountToAgent,
69
- detachAccountFromAgent,
70
- } from '../db/account-attach.js';
71
- import {
72
- createConnectorForUser,
73
- createPendingConnectorForUser,
74
- deleteConnectorForUser,
75
- disconnectConnectorForUser,
76
- connectorNamesByIds,
77
- connectorSummariesByIds,
78
- getConnectorForUser,
79
- listConnectorsForUser,
80
- renameConnectorForUser,
81
- reconnectConnectorForUser,
82
- verifyConnectorForUser,
83
- } from '../db/connectors.js';
84
- import { relayTarget } from '../db/connector-relay.js';
85
- import {
86
- addMemberForUser,
87
- createProjectForUser,
88
- deleteProjectForUser,
89
- listMembersForUser,
90
- listProjectsForUser,
91
- removeMemberForUser,
92
- renameProjectForUser,
93
- setMemberRoleForUser,
94
- } from '../db/projects.js';
95
- import {
96
- addConnectorToAgentForUser,
97
- agentConnectorsForUser,
98
- connectorIdsByAgent,
99
- removeConnectorFromAgentForUser,
100
- } from '../db/agent-connectors.js';
101
- import type { AgentConnectorApiDeps } from './agent-connector-api.js';
43
+ deleteVaultForOwner,
44
+ getVaultForOwner,
45
+ listVaultForOwner,
46
+ putVaultForOwner,
47
+ } from '../db/vault.js';
102
48
  import type { StationName } from '../db/schema.js';
103
- import { AttachSessions } from './attach-session.js';
104
49
  import { startUploadReaper } from './upload-store.js';
105
- import type { AgentApiDeps } from './agent-api.js';
106
- import type { ConnectorApiDeps } from './connector-api.js';
107
- import { deleteVaultForOwner, getVaultForOwner, listVaultForOwner, putVaultForOwner } from '../db/vault.js';
108
50
 
109
51
  installCrashGuard();
110
52
  acquireLock(join(STATE_DIR, '.tail-lock'));
@@ -135,19 +77,13 @@ setTrainCallBackend((train, action, args) =>
135
77
  supervisor.call(train, action, args),
136
78
  );
137
79
 
138
- const runtime = runtimeConfigFromEnv();
139
- const linkedSource = runtime === null ? null : httpSource(runtime);
140
- const localSource = linkedSource ?? (isLocalMode() ? fileSource : null);
141
-
142
80
  function announceLocalEndpoint(): void {
143
- if (isLocalMode())
144
- process.stderr.write(
145
- `\n${tunnel === null ? localConnectHint(webhookPort(), localOwner()) : tunnelPendingHint()}`,
146
- );
81
+ process.stderr.write(
82
+ `\n${tunnel === null ? localConnectHint(webhookPort(), localOwner()) : tunnelPendingHint()}`,
83
+ );
147
84
  const key = localAgentKey();
148
85
  if (key === null) {
149
- if (isLocalMode()) log.info('no agent on this machine yet');
150
- else log.warn('this agent has no key — reset it in the web UI to connect an agent');
86
+ log.info('no agent on this machine yet');
151
87
  return;
152
88
  }
153
89
  const url = `http://127.0.0.1:${String(webhookPort())}/mcp?token=${key}`;
@@ -156,131 +92,15 @@ function announceLocalEndpoint(): void {
156
92
  );
157
93
  }
158
94
 
159
- async function applyStations(removed: StationName[]): Promise<void> {
160
- for (const station of removed) await supervisor.stopTrain(station);
161
- }
162
-
163
- async function syncLocal(): Promise<void> {
164
- if (localSource === null) return;
165
- const { removed, changed } = await reloadFrom(localSource);
166
- await applyStations(removed);
167
- for (const station of changed) supervisor.requestReload(station);
168
- }
169
-
170
- async function stopLocalStations(): Promise<void> {
171
- for (const station of supervisor.running()) await supervisor.stopTrain(station);
172
- }
173
-
174
- const reloadStations = (): Promise<ReloadedStations> =>
175
- localSource === null ? reloadAccountsFromDb() : reloadFrom(localSource);
176
-
177
95
  async function syncStations(station: StationName): Promise<void> {
178
- const { removed } = await reloadStations();
96
+ const { removed } = await reloadFrom(fileSource);
179
97
  if (stationByName(station)?.hasTrain === false) return;
180
98
  if (removed.includes(station)) await supervisor.stopTrain(station);
181
99
  else supervisor.requestReload(station);
182
100
  }
183
101
 
184
- const attachSessions = new AttachSessions({
185
- authorize: async (owner) => {
186
- await ownedAgentOrThrow(owner.subject, owner.agentId);
187
- },
188
- complete: async (owner, station, config) => {
189
- const ref = await attachAccountToAgent(
190
- owner.subject,
191
- owner.agentId,
192
- station,
193
- config,
194
- );
195
- const activated = await syncStations(station).then(
196
- () => true,
197
- (err: unknown) => {
198
- log.warn(
199
- { station, err: errMsg(err) },
200
- 'attach-session: station reload failed, the change lands at the next boot',
201
- );
202
- return false;
203
- },
204
- );
205
- return { accountId: ref.accountId, activated };
206
- },
207
- });
208
-
209
- async function resetAgentKey(
210
- subject: string,
211
- id: string,
212
- ): Promise<ResetAgentKey> {
213
- const reset = await resetAgentKeyForUser(subject, id);
214
- const closed = await closeAgentSession(id);
215
- log.info(
216
- { agent: reset.name, id: reset.id, sessionClosed: closed },
217
- 'agent-api: key rotated, live session dropped',
218
- );
219
- return reset;
220
- }
221
-
222
- const agentApi: AgentApiDeps = {
223
- attachSessions,
224
- listAgents: listAgentsForUser,
225
- createAgent: createAgentForUser,
226
- deleteAgent: deleteAgentForUser,
227
- resetKey: resetAgentKey,
228
- gatherAccounts: gatherAccountsForAgents,
229
- capabilities: accountStationCapabilities,
230
- liveness: agentLiveness,
231
- releaseRuntime: releaseRuntimeForUser,
232
- connectorIds: connectorIdsByAgent,
233
- runtimes: runtimeLabels,
234
- prepareAccount,
235
- attachAccount: attachAccountToAgent,
236
- detachAccount: detachAccountFromAgent,
237
- syncStations,
238
- };
239
-
240
- const runApi: RunApiDeps = {
241
- claimRuntime,
242
- fenceRuntime,
243
- touchRuntime,
244
- loadAgent: loadAgentForRuntime,
245
- blockedStations: blockedStationsFor,
246
- };
247
-
248
- const agentConnectorApi: AgentConnectorApiDeps = {
249
- agentConnectors: agentConnectorsForUser,
250
- addConnector: addConnectorToAgentForUser,
251
- removeConnector: removeConnectorFromAgentForUser,
252
- mintCode: mintAgentCodeForUser,
253
- };
254
-
255
- const projectApi: ProjectApiDeps = {
256
- listProjects: listProjectsForUser,
257
- createProject: createProjectForUser,
258
- renameProject: renameProjectForUser,
259
- deleteProject: deleteProjectForUser,
260
- listMembers: listMembersForUser,
261
- addMember: addMemberForUser,
262
- setMemberRole: setMemberRoleForUser,
263
- removeMember: removeMemberForUser,
264
- };
265
-
266
- const connectorApi: ConnectorApiDeps = {
267
- listConnectors: listConnectorsForUser,
268
- connectorNamesByIds,
269
- connectorSummariesByIds,
270
- agentConnectors: agentConnectorsForUser,
271
- fenceRuntime,
272
- renameConnector: renameConnectorForUser,
273
- createConnector: createConnectorForUser,
274
- createPendingConnector: createPendingConnectorForUser,
275
- reconnectConnector: reconnectConnectorForUser,
276
- getConnector: getConnectorForUser,
277
- verifyConnector: verifyConnectorForUser,
278
- disconnectConnector: disconnectConnectorForUser,
279
- deleteConnector: deleteConnectorForUser,
280
- };
281
-
282
102
  const hostedMode = (): ModeInfo => ({
283
- mode: linkedSource === null ? 'hosted' : 'linked',
103
+ mode: 'hosted',
284
104
  owner: null,
285
105
  project: null,
286
106
  version: METRO_VERSION,
@@ -290,6 +110,10 @@ function sessionApis(): SessionApis {
290
110
  if (isLocalMode())
291
111
  return localSessionApis({
292
112
  syncStations,
113
+ restart: () => {
114
+ exitCode = RESTART_EXIT;
115
+ onShutdown();
116
+ },
293
117
  closeAgentSession,
294
118
  gatherAccounts: gatherAccountsForAgents,
295
119
  capabilities: accountStationCapabilities,
@@ -297,13 +121,12 @@ function sessionApis(): SessionApis {
297
121
  prepareAccount,
298
122
  });
299
123
  return {
300
- agentApi,
301
- agentConnectorApi,
302
- connectorApi,
303
- projectApi,
304
- runApi,
305
- relayApi: { target: relayTarget, fence: fenceRuntime },
306
- vaultApi: { list: listVaultForOwner, put: putVaultForOwner, get: getVaultForOwner, remove: deleteVaultForOwner },
124
+ vaultApi: {
125
+ list: listVaultForOwner,
126
+ put: putVaultForOwner,
127
+ get: getVaultForOwner,
128
+ remove: deleteVaultForOwner,
129
+ },
307
130
  mode: hostedMode,
308
131
  };
309
132
  }
@@ -312,9 +135,8 @@ async function main(): Promise<void> {
312
135
  if (isLocalMode()) {
313
136
  ensureLocalSessionSecret();
314
137
  applyLocalOwner();
138
+ await materializeFrom(fileSource, { allowEmpty: true });
315
139
  }
316
- if (localSource === null) await materializeFromDb();
317
- else await materializeFrom(localSource, { allowEmpty: linkedSource === null });
318
140
  supervisor.start();
319
141
  const metroMcp = await createMetroMcp();
320
142
  webhookServer = await startWebhookServer(
@@ -325,18 +147,18 @@ async function main(): Promise<void> {
325
147
  );
326
148
  metroMcp.startInbound();
327
149
  startUploadReaper();
328
- if (linkedSource !== null)
329
- startRuntimePoller({ sync: syncLocal, stopAll: stopLocalStations });
330
- if (localSource !== null) announceLocalEndpoint();
150
+ if (isLocalMode()) announceLocalEndpoint();
331
151
  tunnel?.start();
332
152
  log.info(
333
- { tunnel: !!tunnel, trainsDir: trainsDir(), mcp: '/' },
153
+ { tunnel: !!tunnel, trainsDir: trainsDir(), mcp: '/', version: METRO_VERSION },
334
154
  'dispatcher ready',
335
155
  );
336
156
  markDaemonReady();
337
157
  }
338
158
 
339
159
  const SHUTDOWN_TIMEOUT_MS = 3_000;
160
+ const RESTART_EXIT = 75;
161
+ let exitCode = 0;
340
162
  let shuttingDown = false;
341
163
  async function shutdown(): Promise<void> {
342
164
  if (shuttingDown) return;
@@ -356,9 +178,8 @@ async function shutdown(): Promise<void> {
356
178
  }),
357
179
  ]);
358
180
  }
359
- await attachSessions.stop();
360
181
  await supervisor.stop();
361
- process.exit(0);
182
+ process.exit(exitCode);
362
183
  }
363
184
  const onShutdown = (): void => {
364
185
  shutdown().catch((err: unknown) => {