@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.
- package/dist/api.js +8 -142
- package/dist/cli.js +27 -131
- package/dist/control.js +5 -104
- package/dist/plugin.js +2 -4
- package/dist/runtime.js +18 -72
- package/dist/serve.js +3 -2
- package/dist/tail.js +7 -8
- package/dist/update.js +13 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/package.json +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +3 -35
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +7 -22
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +4 -54
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +29 -208
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +0 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +2 -37
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +0 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +4 -6
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +3 -14
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +0 -81
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +119 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +42 -12
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +1 -73
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +1 -172
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +0 -5
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +2 -89
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +3 -262
- package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +2 -140
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +1 -89
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +14 -41
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +5 -5
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +0 -2
- package/runtime/runtime.json +1 -1
- package/dist/prompt.js +0 -61
- package/dist/store.js +0 -93
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +0 -76
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +0 -20
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +0 -141
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +0 -68
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +0 -58
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +0 -214
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +0 -139
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +0 -105
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +0 -143
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +0 -309
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +0 -24
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +0 -105
- 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 {
|
|
2
|
-
import { assertAgentId, localUrl
|
|
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
|
|
26
|
-
if (
|
|
27
|
-
throw new Error(`this machine
|
|
28
|
-
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
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 {
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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 (
|
|
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(
|
|
182
|
+
process.exit(exitCode);
|
|
362
183
|
}
|
|
363
184
|
const onShutdown = (): void => {
|
|
364
185
|
shutdown().catch((err: unknown) => {
|