@stage-labs/metro 0.1.0-beta.30 → 0.1.0-beta.32

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 (59) hide show
  1. package/dist/api.js +12 -12
  2. package/dist/bedrock.js +5 -3
  3. package/dist/cli.js +23 -11
  4. package/dist/runtime.js +4 -1
  5. package/dist/serve.js +57 -0
  6. package/dist/store.js +26 -3
  7. package/package.json +1 -1
  8. package/runtime/node_modules/@metro-labs/mcp/package.json +1 -0
  9. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +5 -5
  10. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +24 -28
  11. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
  12. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
  13. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +34 -10
  14. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +5 -3
  15. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +2 -2
  16. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +119 -72
  17. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +31 -20
  18. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +17 -43
  19. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +8 -8
  20. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +3 -3
  21. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +12 -2
  22. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +11 -4
  23. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +158 -0
  24. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +21 -0
  25. package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +31 -0
  26. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +1 -1
  27. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +10 -0
  28. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +17 -17
  29. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +13 -12
  30. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +15 -6
  31. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +1 -1
  32. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +12 -3
  33. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +27 -0
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +21 -21
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +82 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +43 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +89 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +2 -5
  39. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +15 -16
  40. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +8 -8
  42. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +32 -32
  43. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +250 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +152 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +2 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +16 -7
  47. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +49 -40
  48. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +13 -20
  49. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +8 -21
  50. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +32 -17
  51. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +4 -4
  52. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +1 -1
  53. package/runtime/runtime.json +1 -1
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +0 -20
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +0 -202
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +0 -164
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +0 -208
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +0 -18
  59. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +0 -218
@@ -2,10 +2,10 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { ApiError } from './api-error.js';
3
3
  import { errMsg, log } from './log.js';
4
4
  import {
5
- verifyCliToken,
5
+ verifyAgentToken,
6
6
  verifyRunToken,
7
7
  verifySession,
8
- type CliClaims,
8
+ type AgentClaims,
9
9
  type RunClaims,
10
10
  } from './session.js';
11
11
  import { extractToken } from '../mcp/request-identity.js';
@@ -14,7 +14,7 @@ const BODY_MAX = 4 * 1024;
14
14
  const DRAIN_MAX = 2 * BODY_MAX;
15
15
 
16
16
  export interface ApiSession {
17
- email: string;
17
+ subject: string;
18
18
  }
19
19
 
20
20
  export function cors(req: IncomingMessage): Record<string, string> {
@@ -22,6 +22,7 @@ export function cors(req: IncomingMessage): Record<string, string> {
22
22
  'access-control-allow-origin': req.headers.origin ?? '*',
23
23
  'access-control-allow-methods': 'GET, POST, PUT, DELETE, OPTIONS',
24
24
  'access-control-allow-headers': 'Authorization, Content-Type',
25
+ 'access-control-allow-private-network': 'true',
25
26
  'access-control-max-age': '86400',
26
27
  vary: 'Origin',
27
28
  };
@@ -47,8 +48,8 @@ export function apiSession(req: IncomingMessage): ApiSession | null {
47
48
  const token = extractToken(req) ?? '';
48
49
  if (token === '') return null;
49
50
  try {
50
- const { email } = verifySession(token, secret);
51
- return { email: email.toLowerCase() };
51
+ const { subject } = verifySession(token, secret);
52
+ return { subject: subject.toLowerCase() };
52
53
  } catch {
53
54
  return null;
54
55
  }
@@ -61,22 +62,45 @@ export function runIdentity(req: IncomingMessage): RunClaims | null {
61
62
  if (token === '') return null;
62
63
  try {
63
64
  const claims = verifyRunToken(token, secret);
64
- return { ...claims, email: claims.email.toLowerCase() };
65
+ return { ...claims, subject: claims.subject.toLowerCase() };
65
66
  } catch {
66
67
  return null;
67
68
  }
68
69
  }
69
70
 
70
- export function cliIdentity(req: IncomingMessage): CliClaims | null {
71
+ export interface AgentIdentity extends AgentClaims {
72
+ runtimeId?: string;
73
+ }
74
+
75
+ export function agentIdentity(req: IncomingMessage): AgentIdentity | null {
71
76
  const secret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
72
77
  if (secret === '') return null;
73
78
  const token = extractToken(req) ?? '';
74
79
  if (token === '') return null;
75
80
  try {
76
- const { email, collectionId } = verifyCliToken(token, secret);
77
- return { email: email.toLowerCase(), collectionId };
81
+ const { subject, agentId } = verifyAgentToken(token, secret);
82
+ return { subject: subject.toLowerCase(), agentId };
78
83
  } catch {
79
- return null;
84
+ const run = runIdentity(req);
85
+ if (run === null) return null;
86
+ return { subject: run.subject, agentId: run.agentId, runtimeId: run.runtimeId };
87
+ }
88
+ }
89
+
90
+ export type Fence = (runtimeId: string, agentId: string) => Promise<void>;
91
+
92
+ const RELEASED = 'this machine no longer holds the agent';
93
+
94
+ export async function assertLease(
95
+ who: AgentIdentity,
96
+ fence: Fence,
97
+ ): Promise<void> {
98
+ if (who.runtimeId === undefined) return;
99
+ try {
100
+ await fence(who.runtimeId, who.agentId);
101
+ } catch (err) {
102
+ if (err instanceof ApiError) throw new ApiError(RELEASED, 401);
103
+ throw err;
80
104
  }
81
105
  }
82
106
 
@@ -15,6 +15,7 @@ import { attachmentOwner, recordAttachmentOwner } from './attach-owner.js';
15
15
  import { grantAllows, issueAttachmentGrant } from './attach-grant.js';
16
16
  import { errMsg, log } from './log.js';
17
17
  import { loadTunnelConfig, webhookPort } from './tunnel.js';
18
+ import { isLocalMode } from './paths.js';
18
19
 
19
20
  function authorized(req: IncomingMessage, name: string): boolean {
20
21
  const owner = attachmentOwner(name);
@@ -34,10 +35,11 @@ export function publicBaseUrl(): string | null {
34
35
  return host ? `https://${host}` : null;
35
36
  }
36
37
 
38
+ const servesLocally = (): boolean =>
39
+ (process.env.METRO_RUN_TOKEN?.trim() ?? '') !== '' || isLocalMode();
40
+
37
41
  const localBase = (): string | null =>
38
- (process.env.METRO_RUN_TOKEN?.trim() ?? '') === ''
39
- ? null
40
- : `http://127.0.0.1:${String(webhookPort())}`;
42
+ servesLocally() ? `http://127.0.0.1:${String(webhookPort())}` : null;
41
43
 
42
44
  export const publicBaseOrDefault = (): string =>
43
45
  publicBaseUrl() ?? localBase() ?? DEFAULT_PUBLIC_BASE;
@@ -28,7 +28,7 @@ const MAX_PER_AGENT = 2;
28
28
  const MAX_TOTAL = 40;
29
29
 
30
30
  export interface AttachOwner {
31
- email: string;
31
+ subject: string;
32
32
  agentId: string;
33
33
  }
34
34
 
@@ -109,7 +109,7 @@ export class AttachSessions {
109
109
  private own(owner: AttachOwner, attachId: string): Session {
110
110
  const session = this.sessions.get(attachId);
111
111
  if (
112
- session?.owner.email !== owner.email ||
112
+ session?.owner.subject !== owner.subject ||
113
113
  session.owner.agentId !== owner.agentId
114
114
  )
115
115
  throw missing();
@@ -4,7 +4,13 @@ import { type Server } from 'node:http';
4
4
  import { selfLine, userSelf } from './events.js';
5
5
  import { setTrainCallBackend } from './train-call.js';
6
6
  import { errMsg, log, logFatalSync } from './log.js';
7
- import { acquireLock, loadMetroEnv, STATE_DIR } from './paths.js';
7
+ import {
8
+ acquireLock,
9
+ isLocalMode,
10
+ loadMetroEnv,
11
+ STATE_DIR,
12
+ trainsDir,
13
+ } from './paths.js';
8
14
  import { installCrashGuard, markDaemonReady } from './crash-guard.js';
9
15
  import {
10
16
  loadTunnelConfig,
@@ -12,7 +18,7 @@ import {
12
18
  warnOnLegacyWebhooks,
13
19
  webhookPort,
14
20
  } from './tunnel.js';
15
- import { TrainSupervisor, TRAINS_DIR } from './supervisor.js';
21
+ import { TrainSupervisor } from './supervisor.js';
16
22
  import {
17
23
  makeEmit,
18
24
  startWebhookServer,
@@ -26,10 +32,16 @@ import {
26
32
  touchRuntime,
27
33
  } from '../db/runtimes.js';
28
34
  import {
29
- mintRuntimeCodeForEmail,
30
- releaseRuntimeForEmail,
35
+ blockedStationsFor,
36
+ mintAgentCodeForUser,
37
+ releaseRuntimeForUser,
31
38
  } from '../db/runtime-admin.js';
32
39
  import { loadAgentForRuntime, localAgentKey } from '../db/materialize.js';
40
+ import { fileSource } from '../db/file-source.js';
41
+ import { ensureLocalSessionSecret } from './local-secret.js';
42
+ import { localConnectHint, localSessionApis } from './local-mode.js';
43
+ import type { ModeInfo } from './mode-api.js';
44
+ import type { SessionApis } from './session-apis.js';
33
45
  import {
34
46
  agentLiveness,
35
47
  closeAgentSession,
@@ -47,49 +59,48 @@ import {
47
59
  materializeFromDb,
48
60
  reloadAccountsFromDb,
49
61
  reloadFrom,
62
+ type ReloadedStations,
50
63
  } from '../db/materialize.js';
51
64
  import {
52
65
  httpSource,
53
66
  runtimeConfigFromEnv,
54
67
  startRuntimePoller,
55
68
  } from './runtime-source.js';
56
- import { createAgentForEmail, deleteAgentForEmail, listAgentsForEmail, ownedAgentOrThrow, resetAgentKeyForEmail, type ResetAgentKey,} from '../db/agent-admin.js';
69
+ import { createAgentForUser, deleteAgentForUser, listAgentsForUser, ownedAgentOrThrow, resetAgentKeyForUser, type ResetAgentKey,} from '../db/agent-admin.js';
57
70
  import {
58
71
  attachAccountToAgent,
59
72
  detachAccountFromAgent,
60
73
  } from '../db/account-attach.js';
61
74
  import {
62
- createConnectorForEmail,
63
- createPendingConnectorForEmail,
64
- deleteConnectorForEmail,
65
- disconnectConnectorForEmail,
75
+ createConnectorForUser,
76
+ createPendingConnectorForUser,
77
+ deleteConnectorForUser,
78
+ disconnectConnectorForUser,
66
79
  connectorNamesByIds,
67
- getConnectorForEmail,
68
- listConnectorsForEmail,
69
- renameConnectorForEmail,
70
- reconnectConnectorForEmail,
71
- verifyConnectorForEmail,
80
+ getConnectorForUser,
81
+ listConnectorsForUser,
82
+ renameConnectorForUser,
83
+ reconnectConnectorForUser,
84
+ verifyConnectorForUser,
72
85
  } from '../db/connectors.js';
73
86
  import { relayTarget } from '../db/connector-relay.js';
74
87
  import {
75
- addMemberForEmail,
76
- createProjectForEmail,
77
- deleteProjectForEmail,
78
- listMembersForEmail,
79
- listProjectsForEmail,
80
- removeMemberForEmail,
81
- renameProjectForEmail,
82
- setMemberRoleForEmail,
88
+ addMemberForUser,
89
+ createProjectForUser,
90
+ deleteProjectForUser,
91
+ listMembersForUser,
92
+ listProjectsForUser,
93
+ removeMemberForUser,
94
+ renameProjectForUser,
95
+ setMemberRoleForUser,
83
96
  } from '../db/projects.js';
84
97
  import {
85
- addToCollectionForEmail,
86
- createCollectionForEmail,
87
- deleteCollectionForEmail,
88
- getCollectionForEmail,
89
- listCollectionsForEmail,
90
- removeFromCollectionForEmail,
91
- renameCollectionForEmail,
92
- } from '../db/connector-collections.js';
98
+ addConnectorToAgentForUser,
99
+ agentConnectorsForUser,
100
+ connectorIdsByAgent,
101
+ removeConnectorFromAgentForUser,
102
+ } from '../db/agent-connectors.js';
103
+ import type { AgentConnectorApiDeps } from './agent-connector-api.js';
93
104
  import type { StationName } from '../db/schema.js';
94
105
  import { AttachSessions } from './attach-session.js';
95
106
  import { startUploadReaper } from './upload-store.js';
@@ -125,12 +136,15 @@ setTrainCallBackend((train, action, args) =>
125
136
  );
126
137
 
127
138
  const runtime = runtimeConfigFromEnv();
128
- const localSource = runtime === null ? null : httpSource(runtime);
139
+ const linkedSource = runtime === null ? null : httpSource(runtime);
140
+ const localSource = linkedSource ?? (isLocalMode() ? fileSource : null);
129
141
 
130
142
  function announceLocalEndpoint(): void {
143
+ if (isLocalMode()) process.stderr.write(`\n${localConnectHint(webhookPort())}`);
131
144
  const key = localAgentKey();
132
145
  if (key === null) {
133
- log.warn('this agent has no key — reset it in the web UI to connect an agent');
146
+ if (isLocalMode()) log.info('no agent on this machine yet');
147
+ else log.warn('this agent has no key — reset it in the web UI to connect an agent');
134
148
  return;
135
149
  }
136
150
  const url = `http://127.0.0.1:${String(webhookPort())}/mcp?token=${key}`;
@@ -154,8 +168,11 @@ async function stopLocalStations(): Promise<void> {
154
168
  for (const station of supervisor.running()) await supervisor.stopTrain(station);
155
169
  }
156
170
 
171
+ const reloadStations = (): Promise<ReloadedStations> =>
172
+ localSource === null ? reloadAccountsFromDb() : reloadFrom(localSource);
173
+
157
174
  async function syncStations(station: StationName): Promise<void> {
158
- const { removed } = await reloadAccountsFromDb();
175
+ const { removed } = await reloadStations();
159
176
  if (stationByName(station)?.hasTrain === false) return;
160
177
  if (removed.includes(station)) await supervisor.stopTrain(station);
161
178
  else supervisor.requestReload(station);
@@ -163,11 +180,11 @@ async function syncStations(station: StationName): Promise<void> {
163
180
 
164
181
  const attachSessions = new AttachSessions({
165
182
  authorize: async (owner) => {
166
- await ownedAgentOrThrow(owner.email, owner.agentId);
183
+ await ownedAgentOrThrow(owner.subject, owner.agentId);
167
184
  },
168
185
  complete: async (owner, station, config) => {
169
186
  const ref = await attachAccountToAgent(
170
- owner.email,
187
+ owner.subject,
171
188
  owner.agentId,
172
189
  station,
173
190
  config,
@@ -187,10 +204,10 @@ const attachSessions = new AttachSessions({
187
204
  });
188
205
 
189
206
  async function resetAgentKey(
190
- email: string,
207
+ subject: string,
191
208
  id: string,
192
209
  ): Promise<ResetAgentKey> {
193
- const reset = await resetAgentKeyForEmail(email, id);
210
+ const reset = await resetAgentKeyForUser(subject, id);
194
211
  const closed = await closeAgentSession(id);
195
212
  log.info(
196
213
  { agent: reset.name, id: reset.id, sessionClosed: closed },
@@ -201,15 +218,15 @@ async function resetAgentKey(
201
218
 
202
219
  const agentApi: AgentApiDeps = {
203
220
  attachSessions,
204
- listAgents: listAgentsForEmail,
205
- createAgent: createAgentForEmail,
206
- deleteAgent: deleteAgentForEmail,
221
+ listAgents: listAgentsForUser,
222
+ createAgent: createAgentForUser,
223
+ deleteAgent: deleteAgentForUser,
207
224
  resetKey: resetAgentKey,
208
225
  gatherAccounts: gatherAccountsForAgents,
209
226
  capabilities: accountStationCapabilities,
210
227
  liveness: agentLiveness,
211
- mintRuntimeCode: mintRuntimeCodeForEmail,
212
- releaseRuntime: releaseRuntimeForEmail,
228
+ releaseRuntime: releaseRuntimeForUser,
229
+ connectorIds: connectorIdsByAgent,
213
230
  runtimes: runtimeLabels,
214
231
  prepareAccount,
215
232
  attachAccount: attachAccountToAgent,
@@ -222,60 +239,90 @@ const runApi: RunApiDeps = {
222
239
  fenceRuntime,
223
240
  touchRuntime,
224
241
  loadAgent: loadAgentForRuntime,
242
+ blockedStations: blockedStationsFor,
243
+ };
244
+
245
+ const agentConnectorApi: AgentConnectorApiDeps = {
246
+ agentConnectors: agentConnectorsForUser,
247
+ addConnector: addConnectorToAgentForUser,
248
+ removeConnector: removeConnectorFromAgentForUser,
249
+ mintCode: mintAgentCodeForUser,
225
250
  };
226
251
 
227
252
  const projectApi: ProjectApiDeps = {
228
- listProjects: listProjectsForEmail,
229
- createProject: createProjectForEmail,
230
- renameProject: renameProjectForEmail,
231
- deleteProject: deleteProjectForEmail,
232
- listMembers: listMembersForEmail,
233
- addMember: addMemberForEmail,
234
- setMemberRole: setMemberRoleForEmail,
235
- removeMember: removeMemberForEmail,
253
+ listProjects: listProjectsForUser,
254
+ createProject: createProjectForUser,
255
+ renameProject: renameProjectForUser,
256
+ deleteProject: deleteProjectForUser,
257
+ listMembers: listMembersForUser,
258
+ addMember: addMemberForUser,
259
+ setMemberRole: setMemberRoleForUser,
260
+ removeMember: removeMemberForUser,
236
261
  };
237
262
 
238
263
  const connectorApi: ConnectorApiDeps = {
239
- listConnectors: listConnectorsForEmail,
264
+ listConnectors: listConnectorsForUser,
240
265
  connectorNamesByIds,
241
- listCollections: listCollectionsForEmail,
242
- getCollection: getCollectionForEmail,
243
- createCollection: createCollectionForEmail,
244
- renameCollection: renameCollectionForEmail,
245
- deleteCollection: deleteCollectionForEmail,
246
- addToCollection: addToCollectionForEmail,
247
- removeFromCollection: removeFromCollectionForEmail,
248
- renameConnector: renameConnectorForEmail,
249
- createConnector: createConnectorForEmail,
250
- createPendingConnector: createPendingConnectorForEmail,
251
- reconnectConnector: reconnectConnectorForEmail,
252
- getConnector: getConnectorForEmail,
253
- verifyConnector: verifyConnectorForEmail,
254
- disconnectConnector: disconnectConnectorForEmail,
255
- deleteConnector: deleteConnectorForEmail,
266
+ agentConnectors: agentConnectorsForUser,
267
+ fenceRuntime,
268
+ renameConnector: renameConnectorForUser,
269
+ createConnector: createConnectorForUser,
270
+ createPendingConnector: createPendingConnectorForUser,
271
+ reconnectConnector: reconnectConnectorForUser,
272
+ getConnector: getConnectorForUser,
273
+ verifyConnector: verifyConnectorForUser,
274
+ disconnectConnector: disconnectConnectorForUser,
275
+ deleteConnector: deleteConnectorForUser,
256
276
  };
257
277
 
278
+ const hostedMode = (): ModeInfo => ({
279
+ mode: linkedSource === null ? 'hosted' : 'linked',
280
+ owner: null,
281
+ project: null,
282
+ });
283
+
284
+ function sessionApis(): SessionApis {
285
+ if (isLocalMode())
286
+ return localSessionApis({
287
+ syncStations,
288
+ closeAgentSession,
289
+ gatherAccounts: gatherAccountsForAgents,
290
+ capabilities: accountStationCapabilities,
291
+ liveness: agentLiveness,
292
+ prepareAccount,
293
+ });
294
+ return {
295
+ agentApi,
296
+ agentConnectorApi,
297
+ connectorApi,
298
+ projectApi,
299
+ runApi,
300
+ relayApi: { target: relayTarget, fence: fenceRuntime },
301
+ mode: hostedMode,
302
+ };
303
+ }
304
+
258
305
  async function main(): Promise<void> {
306
+ if (isLocalMode()) ensureLocalSessionSecret();
259
307
  if (localSource === null) await materializeFromDb();
260
- else await materializeFrom(localSource);
308
+ else await materializeFrom(localSource, { allowEmpty: linkedSource === null });
261
309
  warnOnLegacyWebhooks();
262
310
  supervisor.start();
263
311
  const metroMcp = await createMetroMcp();
264
312
  webhookServer = await startWebhookServer(
265
313
  emit,
266
- { agentApi, connectorApi, projectApi, runApi, relayApi: { target: relayTarget } },
314
+ sessionApis(),
267
315
  metroMcp.httpHandler,
268
316
  metroCall,
269
317
  );
270
318
  metroMcp.startInbound();
271
319
  startUploadReaper();
272
- if (localSource !== null) {
320
+ if (linkedSource !== null)
273
321
  startRuntimePoller({ sync: syncLocal, stopAll: stopLocalStations });
274
- announceLocalEndpoint();
275
- }
322
+ if (localSource !== null) announceLocalEndpoint();
276
323
  tunnel?.start();
277
324
  log.info(
278
- { tunnel: !!tunnel, trainsDir: TRAINS_DIR, mcp: '/' },
325
+ { tunnel: !!tunnel, trainsDir: trainsDir(), mcp: '/' },
279
326
  'dispatcher ready',
280
327
  );
281
328
  markDaemonReady();
@@ -1,18 +1,20 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import { errMsg, log } from './log.js';
2
+ import { log } from './log.js';
3
3
  import {
4
+ agentIdentity,
5
+ apiFailure,
6
+ assertLease,
4
7
  bodyField,
5
- cliIdentity,
6
8
  cors,
7
9
  readJsonBody,
8
10
  sendJson,
9
11
  } from './api-http.js';
10
12
  import { extractToken } from '../mcp/request-identity.js';
11
13
  import { publicBaseOrDefault } from './attach-serve.js';
12
- import { CLI_CODE_RE, takeCliCode } from './cli-pair.js';
14
+ import { AGENT_CODE_RE, takeAgentCode } from './agent-pair.js';
13
15
  import { relayServersJson } from './connector-json.js';
14
- import { sessionTtlFromEnv } from './google-oauth.js';
15
- import { signCliToken } from './session.js';
16
+ import { sessionTtlFromEnv } from './session-config.js';
17
+ import { signAgentToken } from './session.js';
16
18
  import type { ConnectorApiDeps } from './connector-api.js';
17
19
 
18
20
  const CLAIM_PATH = '/api/cli/claim';
@@ -42,28 +44,33 @@ async function handleClaim(
42
44
  }
43
45
  const raw = bodyField(await readJsonBody(req), 'code');
44
46
  const code = typeof raw === 'string' ? raw.trim() : '';
45
- if (!CLI_CODE_RE.test(code)) {
47
+ if (!AGENT_CODE_RE.test(code)) {
46
48
  sendJson(req, res, 400, {
47
49
  error:
48
- 'that does not look like a collection code — metro login wants the mc_… code from metro.box/#/authorize',
50
+ 'that does not look like an agent code — metro login wants the ma_… code from the agent page',
49
51
  });
50
52
  return;
51
53
  }
52
- const taken = takeCliCode(code);
54
+ const taken = takeAgentCode(code);
53
55
  if (taken === undefined) {
54
56
  sendJson(req, res, 400, {
55
57
  error: 'that code has expired or was already used',
56
58
  });
57
59
  return;
58
60
  }
59
- const collection = await deps.getCollection(taken.email, taken.collectionId);
60
- const token = signCliToken(
61
- { email: taken.email, collectionId: collection.id },
61
+ const agent = await deps.agentConnectors(taken.subject, taken.agentId);
62
+ const token = signAgentToken(
63
+ { subject: taken.subject, agentId: agent.id },
62
64
  secret,
63
65
  { ttlSec: sessionTtlFromEnv() },
64
66
  );
65
- log.info({ email: taken.email, collection: collection.id }, 'cli: code claimed');
66
- sendJson(req, res, 200, { token, email: taken.email, collection: collection.name });
67
+ log.info({ subject: taken.subject, agent: agent.id }, 'cli: code claimed');
68
+ sendJson(req, res, 200, {
69
+ token,
70
+ email: taken.subject,
71
+ subject: taken.subject,
72
+ agent: agent.name,
73
+ });
67
74
  }
68
75
 
69
76
  async function handleRead(
@@ -72,23 +79,28 @@ async function handleRead(
72
79
  deps: ConnectorApiDeps,
73
80
  path: string,
74
81
  ): Promise<void> {
75
- const who = cliIdentity(req);
82
+ const who = agentIdentity(req);
76
83
  if (who === null) {
77
84
  sendJson(req, res, 401, { error: 'unauthorized' });
78
85
  return;
79
86
  }
80
- const collection = await deps.getCollection(who.email, who.collectionId);
87
+ await assertLease(who, deps.fenceRuntime);
88
+ const agent = await deps.agentConnectors(who.subject, who.agentId);
81
89
  if (path === SESSION_PATH) {
82
- sendJson(req, res, 200, { email: who.email, collection: collection.name });
90
+ sendJson(req, res, 200, {
91
+ email: who.subject,
92
+ subject: who.subject,
93
+ agent: agent.name,
94
+ });
83
95
  return;
84
96
  }
85
- const entries = await deps.connectorNamesByIds(collection.connectorIds);
97
+ const entries = await deps.connectorNamesByIds(agent.connectorIds);
86
98
  const json = relayServersJson(
87
99
  entries,
88
100
  publicBaseOrDefault(),
89
101
  extractToken(req) ?? '',
90
102
  );
91
- sendJson(req, res, 200, { json, collection: collection.name });
103
+ sendJson(req, res, 200, { json, agent: agent.name });
92
104
  }
93
105
 
94
106
  export function handleCliPairRequest(
@@ -116,8 +128,7 @@ export function handleCliPairRequest(
116
128
  ? handleClaim(req, res, deps)
117
129
  : handleRead(req, res, deps, path);
118
130
  work.catch((err: unknown) => {
119
- log.warn({ err: errMsg(err) }, 'cli: unhandled error');
120
- if (!res.headersSent) sendJson(req, res, 500, { error: 'cli api failed' });
131
+ apiFailure(req, res, err, 'cli');
121
132
  });
122
133
  return true;
123
134
  }