@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
@@ -1,141 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import { log } from './log.js';
3
- import {
4
- agentIdentity,
5
- apiFailure,
6
- assertLease,
7
- bodyField,
8
- cors,
9
- readJsonBody,
10
- sendJson,
11
- } from './api-http.js';
12
- import { extractToken } from '../mcp/request-identity.js';
13
- import { publicBaseOrDefault } from './attach-serve.js';
14
- import { AGENT_CODE_RE, takeAgentCode } from './agent-pair.js';
15
- import { relayServersJson } from './connector-json.js';
16
- import { SESSION_TTL_SEC } from './session-config.js';
17
- import { signAgentToken } from './session.js';
18
- import type { ConnectorApiDeps } from './connector-api.js';
19
-
20
- const CLAIM_PATH = '/api/cli/claim';
21
- const MCP_PATH = '/api/cli/mcp';
22
- const SESSION_PATH = '/api/cli/session';
23
- const CONNECTORS_PATH = '/api/cli/connectors';
24
-
25
- const ALLOWED: Record<string, string> = {
26
- [CLAIM_PATH]: 'POST',
27
- [MCP_PATH]: 'GET',
28
- [SESSION_PATH]: 'GET',
29
- [CONNECTORS_PATH]: 'GET',
30
- };
31
-
32
- function secretOrNull(): string | null {
33
- const secret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
34
- return secret === '' ? null : secret;
35
- }
36
-
37
- async function handleClaim(
38
- req: IncomingMessage,
39
- res: ServerResponse,
40
- deps: ConnectorApiDeps,
41
- ): Promise<void> {
42
- const secret = secretOrNull();
43
- if (secret === null) {
44
- sendJson(req, res, 503, { error: 'sign-in is not configured' });
45
- return;
46
- }
47
- const raw = bodyField(await readJsonBody(req), 'code');
48
- const code = typeof raw === 'string' ? raw.trim() : '';
49
- if (!AGENT_CODE_RE.test(code)) {
50
- sendJson(req, res, 400, {
51
- error:
52
- 'that does not look like an agent code — metro login wants the ma_… code from the agent page',
53
- });
54
- return;
55
- }
56
- const taken = takeAgentCode(code);
57
- if (taken === undefined) {
58
- sendJson(req, res, 400, {
59
- error: 'that code has expired or was already used',
60
- });
61
- return;
62
- }
63
- const agent = await deps.agentConnectors(taken.subject, taken.agentId);
64
- const token = signAgentToken(
65
- { subject: taken.subject, agentId: agent.id },
66
- secret,
67
- { ttlSec: SESSION_TTL_SEC },
68
- );
69
- log.info({ subject: taken.subject, agent: agent.id }, 'cli: code claimed');
70
- sendJson(req, res, 200, {
71
- token,
72
- subject: taken.subject,
73
- agent: agent.name,
74
- });
75
- }
76
-
77
- async function handleRead(
78
- req: IncomingMessage,
79
- res: ServerResponse,
80
- deps: ConnectorApiDeps,
81
- path: string,
82
- ): Promise<void> {
83
- const who = agentIdentity(req);
84
- if (who === null) {
85
- sendJson(req, res, 401, { error: 'unauthorized' });
86
- return;
87
- }
88
- await assertLease(who, deps.fenceRuntime);
89
- const agent = await deps.agentConnectors(who.subject, who.agentId);
90
- if (path === SESSION_PATH) {
91
- sendJson(req, res, 200, {
92
- subject: who.subject,
93
- agent: agent.name,
94
- });
95
- return;
96
- }
97
- if (path === CONNECTORS_PATH) {
98
- sendJson(req, res, 200, {
99
- agent: agent.name,
100
- connectors: await deps.connectorSummariesByIds(agent.connectorIds),
101
- });
102
- return;
103
- }
104
- const entries = await deps.connectorNamesByIds(agent.connectorIds);
105
- const json = relayServersJson(
106
- entries,
107
- publicBaseOrDefault(),
108
- extractToken(req) ?? '',
109
- );
110
- sendJson(req, res, 200, { json, agent: agent.name });
111
- }
112
-
113
- export function handleCliPairRequest(
114
- req: IncomingMessage,
115
- res: ServerResponse,
116
- deps: ConnectorApiDeps,
117
- ): boolean {
118
- const path = (req.url ?? '').split('?')[0] ?? '';
119
- const method = ALLOWED[path];
120
- if (method === undefined) {
121
- if (!path.startsWith('/api/cli/') && path !== '/api/cli') return false;
122
- sendJson(req, res, 404, { error: 'no such route' });
123
- return true;
124
- }
125
- if (req.method === 'OPTIONS') {
126
- res.writeHead(204, cors(req)).end();
127
- return true;
128
- }
129
- if (req.method !== method) {
130
- sendJson(req, res, 405, { error: 'method not allowed' });
131
- return true;
132
- }
133
- const work =
134
- path === CLAIM_PATH
135
- ? handleClaim(req, res, deps)
136
- : handleRead(req, res, deps, path);
137
- work.catch((err: unknown) => {
138
- apiFailure(req, res, err, 'cli');
139
- });
140
- return true;
141
- }
@@ -1,68 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import { errMsg, log } from './log.js';
3
- import {
4
- apiFailure,
5
- apiSession,
6
- bodyField,
7
- cors,
8
- readJsonBody,
9
- sendJson,
10
- } from './api-http.js';
11
- import { parsePairingCode } from './agent-import.js';
12
- import type { CreatedAgent } from '../db/agent-admin.js';
13
-
14
- const PATH = '/api/agents/import';
15
-
16
- export interface ImportedAgent extends CreatedAgent {
17
- stations: number;
18
- connectors: number;
19
- }
20
-
21
- export interface ImportApiDeps {
22
- importAgent: (subject: string, code: string) => Promise<ImportedAgent>;
23
- }
24
-
25
- async function handle(
26
- req: IncomingMessage,
27
- res: ServerResponse,
28
- deps: ImportApiDeps,
29
- subject: string,
30
- ): Promise<void> {
31
- try {
32
- const code = parsePairingCode(bodyField(await readJsonBody(req), 'code'));
33
- const made = await deps.importAgent(subject, code);
34
- log.info(
35
- { agent: made.name, id: made.id, stations: made.stations, connectors: made.connectors },
36
- 'import-api: agent imported from metro',
37
- );
38
- sendJson(req, res, 201, made);
39
- } catch (err) {
40
- apiFailure(req, res, err, 'import-api');
41
- }
42
- }
43
-
44
- export function handleImportRequest(
45
- req: IncomingMessage,
46
- res: ServerResponse,
47
- deps: ImportApiDeps,
48
- ): boolean {
49
- if ((req.url ?? '').split('?')[0] !== PATH) return false;
50
- if (req.method === 'OPTIONS') {
51
- res.writeHead(204, cors(req)).end();
52
- return true;
53
- }
54
- if (req.method !== 'POST') {
55
- sendJson(req, res, 405, { error: 'method not allowed' });
56
- return true;
57
- }
58
- const session = apiSession(req);
59
- if (!session) {
60
- sendJson(req, res, 401, { error: 'unauthorized' });
61
- return true;
62
- }
63
- handle(req, res, deps, session.subject).catch((err: unknown) => {
64
- log.warn({ err: errMsg(err) }, 'import-api: unhandled error');
65
- if (!res.headersSent) sendJson(req, res, 500, { error: 'import failed' });
66
- });
67
- return true;
68
- }
@@ -1,58 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
-
3
- const TTL_MS = 10 * 60_000;
4
- const SWEEP_MS = 60_000;
5
- const MAX_CODES = 100;
6
-
7
- export interface CodeStore<T> {
8
- pattern: RegExp;
9
- mint(entry: T, now?: number): { code: string; expiresAt: number };
10
- take(code: string, now?: number): T | undefined;
11
- count(): number;
12
- }
13
-
14
- export function makeCodeStore<T extends object>(prefix: string): CodeStore<T> {
15
- const codes = new Map<string, T & { expiresAt: number }>();
16
- let sweeper: ReturnType<typeof setInterval> | null = null;
17
-
18
- const sweep = (now = Date.now()): void => {
19
- for (const [code, entry] of codes)
20
- if (entry.expiresAt <= now) codes.delete(code);
21
- };
22
-
23
- const ensureSweeper = (): void => {
24
- if (sweeper !== null) return;
25
- sweeper = setInterval(sweep, SWEEP_MS);
26
- sweeper.unref?.();
27
- };
28
-
29
- return {
30
- pattern: new RegExp(`^${prefix}_[A-Za-z0-9_-]{16}$`),
31
- mint(entry, now = Date.now()) {
32
- sweep(now);
33
- while (codes.size >= MAX_CODES) {
34
- const oldest = codes.keys().next();
35
- if (oldest.done) break;
36
- codes.delete(oldest.value);
37
- }
38
- const code = `${prefix}_${randomBytes(12).toString('base64url')}`;
39
- const expiresAt = now + TTL_MS;
40
- codes.set(code, { ...entry, expiresAt });
41
- ensureSweeper();
42
- return { code, expiresAt };
43
- },
44
- take(code, now = Date.now()) {
45
- const entry = codes.get(code);
46
- if (entry === undefined) return undefined;
47
- codes.delete(code);
48
- if (entry.expiresAt <= now) return undefined;
49
- const rest: Record<string, unknown> = { ...entry };
50
- delete rest.expiresAt;
51
- return rest as T;
52
- },
53
- count() {
54
- sweep();
55
- return codes.size;
56
- },
57
- };
58
- }
@@ -1,214 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import { errMsg, log } from './log.js';
3
- import {
4
- apiFailure,
5
- apiSession,
6
- bodyField,
7
- cors,
8
- readJsonBody,
9
- sendJson,
10
- type ApiSession,
11
- } from './api-http.js';
12
- import { parseId } from '../db/ids.js';
13
- import type { Member, Project } from '../db/projects.js';
14
-
15
- const PREFIX = '/api/projects';
16
-
17
- export interface ProjectApiDeps {
18
- listProjects: (subject: string) => Promise<Project[]>;
19
- createProject: (subject: string, name: unknown) => Promise<Project>;
20
- renameProject: (
21
- subject: string,
22
- id: string,
23
- name: unknown,
24
- ) => Promise<Project>;
25
- deleteProject: (
26
- subject: string,
27
- id: string,
28
- ) => Promise<{ id: string; name: string }>;
29
- listMembers: (subject: string, id: string) => Promise<Member[]>;
30
- addMember: (
31
- subject: string,
32
- id: string,
33
- invited: unknown,
34
- role: unknown,
35
- ) => Promise<Member[]>;
36
- setMemberRole: (
37
- subject: string,
38
- id: string,
39
- memberId: string,
40
- role: unknown,
41
- ) => Promise<Member[]>;
42
- removeMember: (
43
- subject: string,
44
- id: string,
45
- memberId: string,
46
- ) => Promise<Member[]>;
47
- }
48
-
49
- type Routable =
50
- | { kind: 'index' }
51
- | { kind: 'project'; id: string }
52
- | { kind: 'rename'; id: string }
53
- | { kind: 'members'; id: string }
54
- | { kind: 'member'; id: string; memberId: string };
55
-
56
- type Target = Routable | { kind: 'unknown' } | null;
57
-
58
- const ALLOWED: Record<Routable['kind'], string[]> = {
59
- index: ['GET', 'POST'],
60
- project: ['DELETE'],
61
- rename: ['POST'],
62
- members: ['GET', 'POST'],
63
- member: ['POST', 'DELETE'],
64
- };
65
-
66
- function memberTarget(id: string, rest: string[]): Target {
67
- if (rest.length === 1) return { kind: 'members', id };
68
- if (rest.length > 2) return { kind: 'unknown' };
69
- const memberId = parseId(rest[1] ?? '');
70
- return memberId === null
71
- ? { kind: 'unknown' }
72
- : { kind: 'member', id, memberId };
73
- }
74
-
75
- function subTarget(id: string, rest: string[]): Target {
76
- if (rest.length === 0) return { kind: 'project', id };
77
- const head = rest[0];
78
- if (head === 'members') return memberTarget(id, rest);
79
- if (rest.length > 1) return { kind: 'unknown' };
80
- if (head === 'rename') return { kind: 'rename', id };
81
- return { kind: 'unknown' };
82
- }
83
-
84
- function target(path: string): Target {
85
- if (path === PREFIX || path === `${PREFIX}/`) return { kind: 'index' };
86
- if (!path.startsWith(`${PREFIX}/`)) return null;
87
- const segments = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
88
- const head = segments[0];
89
- if (head === undefined) return { kind: 'index' };
90
- const id = parseId(head);
91
- return id === null ? { kind: 'unknown' } : subTarget(id, segments.slice(1));
92
- }
93
-
94
- async function handleIndex(
95
- req: IncomingMessage,
96
- res: ServerResponse,
97
- deps: ProjectApiDeps,
98
- session: ApiSession,
99
- ): Promise<void> {
100
- if (req.method === 'GET') {
101
- sendJson(req, res, 200, { projects: await deps.listProjects(session.subject) });
102
- return;
103
- }
104
- const made = await deps.createProject(
105
- session.subject,
106
- bodyField(await readJsonBody(req), 'name'),
107
- );
108
- log.info({ id: made.id, name: made.name }, 'project-api: created project');
109
- sendJson(req, res, 201, made);
110
- }
111
-
112
- async function handleMembers(
113
- req: IncomingMessage,
114
- res: ServerResponse,
115
- deps: ProjectApiDeps,
116
- session: ApiSession,
117
- id: string,
118
- ): Promise<void> {
119
- if (req.method === 'GET') {
120
- sendJson(req, res, 200, { members: await deps.listMembers(session.subject, id) });
121
- return;
122
- }
123
- const body = await readJsonBody(req);
124
- const members = await deps.addMember(
125
- session.subject,
126
- id,
127
- bodyField(body, 'address'),
128
- bodyField(body, 'role'),
129
- );
130
- sendJson(req, res, 200, { members });
131
- }
132
-
133
- async function handleMember(
134
- req: IncomingMessage,
135
- res: ServerResponse,
136
- deps: ProjectApiDeps,
137
- session: ApiSession,
138
- tgt: { id: string; memberId: string },
139
- ): Promise<void> {
140
- if (req.method === 'DELETE') {
141
- const members = await deps.removeMember(session.subject, tgt.id, tgt.memberId);
142
- sendJson(req, res, 200, { members });
143
- return;
144
- }
145
- const members = await deps.setMemberRole(
146
- session.subject,
147
- tgt.id,
148
- tgt.memberId,
149
- bodyField(await readJsonBody(req), 'role'),
150
- );
151
- sendJson(req, res, 200, { members });
152
- }
153
-
154
- async function route(
155
- req: IncomingMessage,
156
- res: ServerResponse,
157
- deps: ProjectApiDeps,
158
- session: ApiSession,
159
- tgt: Routable,
160
- ): Promise<void> {
161
- try {
162
- if (tgt.kind === 'index') await handleIndex(req, res, deps, session);
163
- else if (tgt.kind === 'members')
164
- await handleMembers(req, res, deps, session, tgt.id);
165
- else if (tgt.kind === 'member')
166
- await handleMember(req, res, deps, session, tgt);
167
- else if (tgt.kind === 'rename')
168
- sendJson(
169
- req,
170
- res,
171
- 200,
172
- await deps.renameProject(
173
- session.subject,
174
- tgt.id,
175
- bodyField(await readJsonBody(req), 'name'),
176
- ),
177
- );
178
- else sendJson(req, res, 200, await deps.deleteProject(session.subject, tgt.id));
179
- } catch (err) {
180
- apiFailure(req, res, err, 'project-api');
181
- }
182
- }
183
-
184
- export function handleProjectApiRequest(
185
- req: IncomingMessage,
186
- res: ServerResponse,
187
- deps: ProjectApiDeps,
188
- ): boolean {
189
- const tgt = target((req.url ?? '').split('?')[0] ?? '');
190
- if (tgt === null) return false;
191
- if (req.method === 'OPTIONS') {
192
- res.writeHead(204, cors(req)).end();
193
- return true;
194
- }
195
- if (tgt.kind === 'unknown') {
196
- sendJson(req, res, 404, { error: 'no such project' });
197
- return true;
198
- }
199
- if (!ALLOWED[tgt.kind].includes(req.method ?? '')) {
200
- sendJson(req, res, 405, { error: 'method not allowed' });
201
- return true;
202
- }
203
- const session = apiSession(req);
204
- if (!session) {
205
- sendJson(req, res, 401, { error: 'unauthorized' });
206
- return true;
207
- }
208
- route(req, res, deps, session, tgt).catch((err: unknown) => {
209
- log.warn({ err: errMsg(err) }, 'project-api: unhandled error');
210
- if (!res.headersSent)
211
- sendJson(req, res, 500, { error: 'project api failed' });
212
- });
213
- return true;
214
- }
@@ -1,139 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import { log } from './log.js';
3
- import {
4
- apiFailure,
5
- bodyField,
6
- cors,
7
- readJsonBody,
8
- runIdentity,
9
- sendJson,
10
- } from './api-http.js';
11
- import { AGENT_CODE_RE, takeAgentCode } from './agent-pair.js';
12
- import { SESSION_TTL_SEC } from './session-config.js';
13
- import { signRunToken } from './session.js';
14
- import type { LoadedAgent } from '../db/materialize.js';
15
- import type { RuntimeLease } from '../db/runtimes.js';
16
-
17
- const CLAIM_PATH = '/api/run/claim';
18
- const CONFIG_PATH = '/api/run/config';
19
-
20
- const ALLOWED: Record<string, string> = {
21
- [CLAIM_PATH]: 'POST',
22
- [CONFIG_PATH]: 'GET',
23
- };
24
-
25
- export interface RunApiDeps {
26
- claimRuntime: (agentId: string, label: string) => Promise<RuntimeLease>;
27
- fenceRuntime: (runtimeId: string, agentId: string) => Promise<void>;
28
- touchRuntime: (runtimeId: string) => Promise<void>;
29
- loadAgent: (agentId: string) => Promise<LoadedAgent>;
30
- blockedStations: (agentId: string) => Promise<string[]>;
31
- }
32
-
33
- function secretOrNull(): string | null {
34
- const secret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
35
- return secret === '' ? null : secret;
36
- }
37
-
38
- function labelOf(body: unknown): string {
39
- const raw = bodyField(body, 'label');
40
- const label = typeof raw === 'string' ? raw.trim().slice(0, 64) : '';
41
- return label === '' ? 'unnamed machine' : label;
42
- }
43
-
44
- async function handleClaim(
45
- req: IncomingMessage,
46
- res: ServerResponse,
47
- deps: RunApiDeps,
48
- ): Promise<void> {
49
- const secret = secretOrNull();
50
- if (secret === null) {
51
- sendJson(req, res, 503, { error: 'sign-in is not configured' });
52
- return;
53
- }
54
- const body = await readJsonBody(req);
55
- const raw = bodyField(body, 'code');
56
- const code = typeof raw === 'string' ? raw.trim() : '';
57
- if (!AGENT_CODE_RE.test(code)) {
58
- sendJson(req, res, 400, {
59
- error:
60
- 'that does not look like an agent code — metro start wants the ma_… code from the agent page',
61
- });
62
- return;
63
- }
64
- const taken = takeAgentCode(code);
65
- if (taken === undefined) {
66
- sendJson(req, res, 400, {
67
- error: 'that code has expired or was already used',
68
- });
69
- return;
70
- }
71
- const blocked = await deps.blockedStations(taken.agentId);
72
- if (blocked.length > 0) {
73
- sendJson(req, res, 409, {
74
- error:
75
- `this agent holds ${blocked.join(', ')}, which only runs on metro because a webhook url has to be publicly reachable. Detach it, or move it to a second agent that stays on metro, then start this one.`,
76
- });
77
- return;
78
- }
79
- const lease = await deps.claimRuntime(taken.agentId, labelOf(body));
80
- const token = signRunToken(
81
- { subject: taken.subject, agentId: lease.agentId, runtimeId: lease.runtimeId },
82
- secret,
83
- { ttlSec: SESSION_TTL_SEC },
84
- );
85
- log.info(
86
- { agent: lease.agentId, runtime: lease.runtimeId, label: lease.label },
87
- 'run: agent claimed by a local runtime',
88
- );
89
- sendJson(req, res, 200, {
90
- token,
91
- agent: lease.agentId,
92
- label: lease.label,
93
- });
94
- }
95
-
96
- async function handleConfig(
97
- req: IncomingMessage,
98
- res: ServerResponse,
99
- deps: RunApiDeps,
100
- ): Promise<void> {
101
- const who = runIdentity(req);
102
- if (who === null) {
103
- sendJson(req, res, 401, { error: 'unauthorized' });
104
- return;
105
- }
106
- await deps.fenceRuntime(who.runtimeId, who.agentId);
107
- await deps.touchRuntime(who.runtimeId);
108
- sendJson(req, res, 200, { agent: await deps.loadAgent(who.agentId) });
109
- }
110
-
111
- export function handleRunApiRequest(
112
- req: IncomingMessage,
113
- res: ServerResponse,
114
- deps: RunApiDeps,
115
- ): boolean {
116
- const path = (req.url ?? '').split('?')[0] ?? '';
117
- const method = ALLOWED[path];
118
- if (method === undefined) {
119
- if (!path.startsWith('/api/run/') && path !== '/api/run') return false;
120
- sendJson(req, res, 404, { error: 'no such route' });
121
- return true;
122
- }
123
- if (req.method === 'OPTIONS') {
124
- res.writeHead(204, cors(req)).end();
125
- return true;
126
- }
127
- if (req.method !== method) {
128
- sendJson(req, res, 405, { error: 'method not allowed' });
129
- return true;
130
- }
131
- const work =
132
- path === CLAIM_PATH
133
- ? handleClaim(req, res, deps)
134
- : handleConfig(req, res, deps);
135
- work.catch((err: unknown) => {
136
- apiFailure(req, res, err, 'run-api');
137
- });
138
- return true;
139
- }