@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.
- package/dist/api.js +12 -12
- package/dist/bedrock.js +5 -3
- package/dist/cli.js +23 -11
- package/dist/runtime.js +4 -1
- package/dist/serve.js +57 -0
- package/dist/store.js +26 -3
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/package.json +1 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +5 -5
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +24 -28
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +34 -10
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +5 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +119 -72
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +31 -20
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +17 -43
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +8 -8
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +3 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +12 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +11 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +158 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +10 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +17 -17
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +13 -12
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +15 -6
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +12 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +21 -21
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +89 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +2 -5
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +15 -16
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +8 -8
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +32 -32
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +250 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +152 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +16 -7
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +49 -40
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +13 -20
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +8 -21
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +32 -17
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +4 -4
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +1 -1
- package/runtime/runtime.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +0 -20
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +0 -202
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +0 -164
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +0 -208
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +0 -18
- 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
|
-
|
|
5
|
+
verifyAgentToken,
|
|
6
6
|
verifyRunToken,
|
|
7
7
|
verifySession,
|
|
8
|
-
type
|
|
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
|
-
|
|
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 {
|
|
51
|
-
return {
|
|
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,
|
|
65
|
+
return { ...claims, subject: claims.subject.toLowerCase() };
|
|
65
66
|
} catch {
|
|
66
67
|
return null;
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
export
|
|
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 {
|
|
77
|
-
return {
|
|
81
|
+
const { subject, agentId } = verifyAgentToken(token, secret);
|
|
82
|
+
return { subject: subject.toLowerCase(), agentId };
|
|
78
83
|
} catch {
|
|
79
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
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 {
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
createConnectorForUser,
|
|
76
|
+
createPendingConnectorForUser,
|
|
77
|
+
deleteConnectorForUser,
|
|
78
|
+
disconnectConnectorForUser,
|
|
66
79
|
connectorNamesByIds,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
183
|
+
await ownedAgentOrThrow(owner.subject, owner.agentId);
|
|
167
184
|
},
|
|
168
185
|
complete: async (owner, station, config) => {
|
|
169
186
|
const ref = await attachAccountToAgent(
|
|
170
|
-
owner.
|
|
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
|
-
|
|
207
|
+
subject: string,
|
|
191
208
|
id: string,
|
|
192
209
|
): Promise<ResetAgentKey> {
|
|
193
|
-
const reset = await
|
|
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:
|
|
205
|
-
createAgent:
|
|
206
|
-
deleteAgent:
|
|
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
|
-
|
|
212
|
-
|
|
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:
|
|
229
|
-
createProject:
|
|
230
|
-
renameProject:
|
|
231
|
-
deleteProject:
|
|
232
|
-
listMembers:
|
|
233
|
-
addMember:
|
|
234
|
-
setMemberRole:
|
|
235
|
-
removeMember:
|
|
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:
|
|
264
|
+
listConnectors: listConnectorsForUser,
|
|
240
265
|
connectorNamesByIds,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
314
|
+
sessionApis(),
|
|
267
315
|
metroMcp.httpHandler,
|
|
268
316
|
metroCall,
|
|
269
317
|
);
|
|
270
318
|
metroMcp.startInbound();
|
|
271
319
|
startUploadReaper();
|
|
272
|
-
if (
|
|
320
|
+
if (linkedSource !== null)
|
|
273
321
|
startRuntimePoller({ sync: syncLocal, stopAll: stopLocalStations });
|
|
274
|
-
|
|
275
|
-
}
|
|
322
|
+
if (localSource !== null) announceLocalEndpoint();
|
|
276
323
|
tunnel?.start();
|
|
277
324
|
log.info(
|
|
278
|
-
{ tunnel: !!tunnel, trainsDir:
|
|
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 {
|
|
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 {
|
|
14
|
+
import { AGENT_CODE_RE, takeAgentCode } from './agent-pair.js';
|
|
13
15
|
import { relayServersJson } from './connector-json.js';
|
|
14
|
-
import { sessionTtlFromEnv } from './
|
|
15
|
-
import {
|
|
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 (!
|
|
47
|
+
if (!AGENT_CODE_RE.test(code)) {
|
|
46
48
|
sendJson(req, res, 400, {
|
|
47
49
|
error:
|
|
48
|
-
'that does not look like
|
|
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 =
|
|
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
|
|
60
|
-
const token =
|
|
61
|
-
{
|
|
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({
|
|
66
|
-
sendJson(req, res, 200, {
|
|
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 =
|
|
82
|
+
const who = agentIdentity(req);
|
|
76
83
|
if (who === null) {
|
|
77
84
|
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
78
85
|
return;
|
|
79
86
|
}
|
|
80
|
-
|
|
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, {
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
}
|