@stage-labs/metro 0.1.0-beta.31 → 0.1.0-beta.33
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/cli.js +7 -0
- package/dist/runtime.js +4 -1
- package/dist/serve.js +73 -0
- 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 +16 -15
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +9 -9
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +8 -7
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +9 -5
- 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 +124 -73
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +16 -7
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +14 -14
- 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 +144 -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/run-api.ts +2 -2
- 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 +4 -0
- 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 +9 -9
- 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/daemon/tunnel.ts +120 -32
- 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 +9 -9
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +31 -31
- 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 +48 -39
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +7 -7
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +22 -12
- 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/google-auth.ts +0 -164
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +0 -208
|
@@ -4,15 +4,27 @@ 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,
|
|
11
17
|
Tunnel,
|
|
18
|
+
tunnelConfigFromEnv,
|
|
12
19
|
warnOnLegacyWebhooks,
|
|
13
20
|
webhookPort,
|
|
14
21
|
} from './tunnel.js';
|
|
15
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
localConnectHint,
|
|
24
|
+
publicConnectHint,
|
|
25
|
+
tunnelPendingHint,
|
|
26
|
+
} from './connect-hint.js';
|
|
27
|
+
import { TrainSupervisor } from './supervisor.js';
|
|
16
28
|
import {
|
|
17
29
|
makeEmit,
|
|
18
30
|
startWebhookServer,
|
|
@@ -27,10 +39,15 @@ import {
|
|
|
27
39
|
} from '../db/runtimes.js';
|
|
28
40
|
import {
|
|
29
41
|
blockedStationsFor,
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
mintAgentCodeForUser,
|
|
43
|
+
releaseRuntimeForUser,
|
|
32
44
|
} from '../db/runtime-admin.js';
|
|
33
45
|
import { loadAgentForRuntime, localAgentKey } from '../db/materialize.js';
|
|
46
|
+
import { fileSource } from '../db/file-source.js';
|
|
47
|
+
import { ensureLocalSessionSecret } from './local-secret.js';
|
|
48
|
+
import { localSessionApis } from './local-mode.js';
|
|
49
|
+
import type { ModeInfo } from './mode-api.js';
|
|
50
|
+
import type { SessionApis } from './session-apis.js';
|
|
34
51
|
import {
|
|
35
52
|
agentLiveness,
|
|
36
53
|
closeAgentSession,
|
|
@@ -48,45 +65,46 @@ import {
|
|
|
48
65
|
materializeFromDb,
|
|
49
66
|
reloadAccountsFromDb,
|
|
50
67
|
reloadFrom,
|
|
68
|
+
type ReloadedStations,
|
|
51
69
|
} from '../db/materialize.js';
|
|
52
70
|
import {
|
|
53
71
|
httpSource,
|
|
54
72
|
runtimeConfigFromEnv,
|
|
55
73
|
startRuntimePoller,
|
|
56
74
|
} from './runtime-source.js';
|
|
57
|
-
import {
|
|
75
|
+
import { createAgentForUser, deleteAgentForUser, listAgentsForUser, ownedAgentOrThrow, resetAgentKeyForUser, type ResetAgentKey,} from '../db/agent-admin.js';
|
|
58
76
|
import {
|
|
59
77
|
attachAccountToAgent,
|
|
60
78
|
detachAccountFromAgent,
|
|
61
79
|
} from '../db/account-attach.js';
|
|
62
80
|
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
createConnectorForUser,
|
|
82
|
+
createPendingConnectorForUser,
|
|
83
|
+
deleteConnectorForUser,
|
|
84
|
+
disconnectConnectorForUser,
|
|
67
85
|
connectorNamesByIds,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
getConnectorForUser,
|
|
87
|
+
listConnectorsForUser,
|
|
88
|
+
renameConnectorForUser,
|
|
89
|
+
reconnectConnectorForUser,
|
|
90
|
+
verifyConnectorForUser,
|
|
73
91
|
} from '../db/connectors.js';
|
|
74
92
|
import { relayTarget } from '../db/connector-relay.js';
|
|
75
93
|
import {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
addMemberForUser,
|
|
95
|
+
createProjectForUser,
|
|
96
|
+
deleteProjectForUser,
|
|
97
|
+
listMembersForUser,
|
|
98
|
+
listProjectsForUser,
|
|
99
|
+
removeMemberForUser,
|
|
100
|
+
renameProjectForUser,
|
|
101
|
+
setMemberRoleForUser,
|
|
84
102
|
} from '../db/projects.js';
|
|
85
103
|
import {
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
addConnectorToAgentForUser,
|
|
105
|
+
agentConnectorsForUser,
|
|
88
106
|
connectorIdsByAgent,
|
|
89
|
-
|
|
107
|
+
removeConnectorFromAgentForUser,
|
|
90
108
|
} from '../db/agent-connectors.js';
|
|
91
109
|
import type { AgentConnectorApiDeps } from './agent-connector-api.js';
|
|
92
110
|
import type { StationName } from '../db/schema.js';
|
|
@@ -116,20 +134,30 @@ supervisor.onTrainEvent((env, train) => {
|
|
|
116
134
|
});
|
|
117
135
|
|
|
118
136
|
let webhookServer: Server | null = null;
|
|
119
|
-
const tunnelCfg = loadTunnelConfig();
|
|
120
|
-
const tunnel = tunnelCfg ? new Tunnel(tunnelCfg, webhookPort()) : null;
|
|
137
|
+
const tunnelCfg = loadTunnelConfig() ?? tunnelConfigFromEnv();
|
|
138
|
+
const tunnel = tunnelCfg ? new Tunnel(tunnelCfg, webhookPort(), announcePublic) : null;
|
|
139
|
+
|
|
140
|
+
function announcePublic(url: string): void {
|
|
141
|
+
process.stderr.write(`\n${publicConnectHint(url)}`);
|
|
142
|
+
}
|
|
121
143
|
|
|
122
144
|
setTrainCallBackend((train, action, args) =>
|
|
123
145
|
supervisor.call(train, action, args),
|
|
124
146
|
);
|
|
125
147
|
|
|
126
148
|
const runtime = runtimeConfigFromEnv();
|
|
127
|
-
const
|
|
149
|
+
const linkedSource = runtime === null ? null : httpSource(runtime);
|
|
150
|
+
const localSource = linkedSource ?? (isLocalMode() ? fileSource : null);
|
|
128
151
|
|
|
129
152
|
function announceLocalEndpoint(): void {
|
|
153
|
+
if (isLocalMode())
|
|
154
|
+
process.stderr.write(
|
|
155
|
+
`\n${tunnel === null ? localConnectHint(webhookPort()) : tunnelPendingHint()}`,
|
|
156
|
+
);
|
|
130
157
|
const key = localAgentKey();
|
|
131
158
|
if (key === null) {
|
|
132
|
-
log.
|
|
159
|
+
if (isLocalMode()) log.info('no agent on this machine yet');
|
|
160
|
+
else log.warn('this agent has no key — reset it in the web UI to connect an agent');
|
|
133
161
|
return;
|
|
134
162
|
}
|
|
135
163
|
const url = `http://127.0.0.1:${String(webhookPort())}/mcp?token=${key}`;
|
|
@@ -153,8 +181,11 @@ async function stopLocalStations(): Promise<void> {
|
|
|
153
181
|
for (const station of supervisor.running()) await supervisor.stopTrain(station);
|
|
154
182
|
}
|
|
155
183
|
|
|
184
|
+
const reloadStations = (): Promise<ReloadedStations> =>
|
|
185
|
+
localSource === null ? reloadAccountsFromDb() : reloadFrom(localSource);
|
|
186
|
+
|
|
156
187
|
async function syncStations(station: StationName): Promise<void> {
|
|
157
|
-
const { removed } = await
|
|
188
|
+
const { removed } = await reloadStations();
|
|
158
189
|
if (stationByName(station)?.hasTrain === false) return;
|
|
159
190
|
if (removed.includes(station)) await supervisor.stopTrain(station);
|
|
160
191
|
else supervisor.requestReload(station);
|
|
@@ -162,11 +193,11 @@ async function syncStations(station: StationName): Promise<void> {
|
|
|
162
193
|
|
|
163
194
|
const attachSessions = new AttachSessions({
|
|
164
195
|
authorize: async (owner) => {
|
|
165
|
-
await ownedAgentOrThrow(owner.
|
|
196
|
+
await ownedAgentOrThrow(owner.subject, owner.agentId);
|
|
166
197
|
},
|
|
167
198
|
complete: async (owner, station, config) => {
|
|
168
199
|
const ref = await attachAccountToAgent(
|
|
169
|
-
owner.
|
|
200
|
+
owner.subject,
|
|
170
201
|
owner.agentId,
|
|
171
202
|
station,
|
|
172
203
|
config,
|
|
@@ -186,10 +217,10 @@ const attachSessions = new AttachSessions({
|
|
|
186
217
|
});
|
|
187
218
|
|
|
188
219
|
async function resetAgentKey(
|
|
189
|
-
|
|
220
|
+
subject: string,
|
|
190
221
|
id: string,
|
|
191
222
|
): Promise<ResetAgentKey> {
|
|
192
|
-
const reset = await
|
|
223
|
+
const reset = await resetAgentKeyForUser(subject, id);
|
|
193
224
|
const closed = await closeAgentSession(id);
|
|
194
225
|
log.info(
|
|
195
226
|
{ agent: reset.name, id: reset.id, sessionClosed: closed },
|
|
@@ -200,14 +231,14 @@ async function resetAgentKey(
|
|
|
200
231
|
|
|
201
232
|
const agentApi: AgentApiDeps = {
|
|
202
233
|
attachSessions,
|
|
203
|
-
listAgents:
|
|
204
|
-
createAgent:
|
|
205
|
-
deleteAgent:
|
|
234
|
+
listAgents: listAgentsForUser,
|
|
235
|
+
createAgent: createAgentForUser,
|
|
236
|
+
deleteAgent: deleteAgentForUser,
|
|
206
237
|
resetKey: resetAgentKey,
|
|
207
238
|
gatherAccounts: gatherAccountsForAgents,
|
|
208
239
|
capabilities: accountStationCapabilities,
|
|
209
240
|
liveness: agentLiveness,
|
|
210
|
-
releaseRuntime:
|
|
241
|
+
releaseRuntime: releaseRuntimeForUser,
|
|
211
242
|
connectorIds: connectorIdsByAgent,
|
|
212
243
|
runtimes: runtimeLabels,
|
|
213
244
|
prepareAccount,
|
|
@@ -225,66 +256,86 @@ const runApi: RunApiDeps = {
|
|
|
225
256
|
};
|
|
226
257
|
|
|
227
258
|
const agentConnectorApi: AgentConnectorApiDeps = {
|
|
228
|
-
agentConnectors:
|
|
229
|
-
addConnector:
|
|
230
|
-
removeConnector:
|
|
231
|
-
mintCode:
|
|
259
|
+
agentConnectors: agentConnectorsForUser,
|
|
260
|
+
addConnector: addConnectorToAgentForUser,
|
|
261
|
+
removeConnector: removeConnectorFromAgentForUser,
|
|
262
|
+
mintCode: mintAgentCodeForUser,
|
|
232
263
|
};
|
|
233
264
|
|
|
234
265
|
const projectApi: ProjectApiDeps = {
|
|
235
|
-
listProjects:
|
|
236
|
-
createProject:
|
|
237
|
-
renameProject:
|
|
238
|
-
deleteProject:
|
|
239
|
-
listMembers:
|
|
240
|
-
addMember:
|
|
241
|
-
setMemberRole:
|
|
242
|
-
removeMember:
|
|
266
|
+
listProjects: listProjectsForUser,
|
|
267
|
+
createProject: createProjectForUser,
|
|
268
|
+
renameProject: renameProjectForUser,
|
|
269
|
+
deleteProject: deleteProjectForUser,
|
|
270
|
+
listMembers: listMembersForUser,
|
|
271
|
+
addMember: addMemberForUser,
|
|
272
|
+
setMemberRole: setMemberRoleForUser,
|
|
273
|
+
removeMember: removeMemberForUser,
|
|
243
274
|
};
|
|
244
275
|
|
|
245
276
|
const connectorApi: ConnectorApiDeps = {
|
|
246
|
-
listConnectors:
|
|
277
|
+
listConnectors: listConnectorsForUser,
|
|
247
278
|
connectorNamesByIds,
|
|
248
|
-
agentConnectors:
|
|
279
|
+
agentConnectors: agentConnectorsForUser,
|
|
249
280
|
fenceRuntime,
|
|
250
|
-
renameConnector:
|
|
251
|
-
createConnector:
|
|
252
|
-
createPendingConnector:
|
|
253
|
-
reconnectConnector:
|
|
254
|
-
getConnector:
|
|
255
|
-
verifyConnector:
|
|
256
|
-
disconnectConnector:
|
|
257
|
-
deleteConnector:
|
|
281
|
+
renameConnector: renameConnectorForUser,
|
|
282
|
+
createConnector: createConnectorForUser,
|
|
283
|
+
createPendingConnector: createPendingConnectorForUser,
|
|
284
|
+
reconnectConnector: reconnectConnectorForUser,
|
|
285
|
+
getConnector: getConnectorForUser,
|
|
286
|
+
verifyConnector: verifyConnectorForUser,
|
|
287
|
+
disconnectConnector: disconnectConnectorForUser,
|
|
288
|
+
deleteConnector: deleteConnectorForUser,
|
|
258
289
|
};
|
|
259
290
|
|
|
291
|
+
const hostedMode = (): ModeInfo => ({
|
|
292
|
+
mode: linkedSource === null ? 'hosted' : 'linked',
|
|
293
|
+
owner: null,
|
|
294
|
+
project: null,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
function sessionApis(): SessionApis {
|
|
298
|
+
if (isLocalMode())
|
|
299
|
+
return localSessionApis({
|
|
300
|
+
syncStations,
|
|
301
|
+
closeAgentSession,
|
|
302
|
+
gatherAccounts: gatherAccountsForAgents,
|
|
303
|
+
capabilities: accountStationCapabilities,
|
|
304
|
+
liveness: agentLiveness,
|
|
305
|
+
prepareAccount,
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
agentApi,
|
|
309
|
+
agentConnectorApi,
|
|
310
|
+
connectorApi,
|
|
311
|
+
projectApi,
|
|
312
|
+
runApi,
|
|
313
|
+
relayApi: { target: relayTarget, fence: fenceRuntime },
|
|
314
|
+
mode: hostedMode,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
260
318
|
async function main(): Promise<void> {
|
|
319
|
+
if (isLocalMode()) ensureLocalSessionSecret();
|
|
261
320
|
if (localSource === null) await materializeFromDb();
|
|
262
|
-
else await materializeFrom(localSource);
|
|
321
|
+
else await materializeFrom(localSource, { allowEmpty: linkedSource === null });
|
|
263
322
|
warnOnLegacyWebhooks();
|
|
264
323
|
supervisor.start();
|
|
265
324
|
const metroMcp = await createMetroMcp();
|
|
266
325
|
webhookServer = await startWebhookServer(
|
|
267
326
|
emit,
|
|
268
|
-
|
|
269
|
-
agentApi,
|
|
270
|
-
agentConnectorApi,
|
|
271
|
-
connectorApi,
|
|
272
|
-
projectApi,
|
|
273
|
-
runApi,
|
|
274
|
-
relayApi: { target: relayTarget, fence: fenceRuntime },
|
|
275
|
-
},
|
|
327
|
+
sessionApis(),
|
|
276
328
|
metroMcp.httpHandler,
|
|
277
329
|
metroCall,
|
|
278
330
|
);
|
|
279
331
|
metroMcp.startInbound();
|
|
280
332
|
startUploadReaper();
|
|
281
|
-
if (
|
|
333
|
+
if (linkedSource !== null)
|
|
282
334
|
startRuntimePoller({ sync: syncLocal, stopAll: stopLocalStations });
|
|
283
|
-
|
|
284
|
-
}
|
|
335
|
+
if (localSource !== null) announceLocalEndpoint();
|
|
285
336
|
tunnel?.start();
|
|
286
337
|
log.info(
|
|
287
|
-
{ tunnel: !!tunnel, trainsDir:
|
|
338
|
+
{ tunnel: !!tunnel, trainsDir: trainsDir(), mcp: '/' },
|
|
288
339
|
'dispatcher ready',
|
|
289
340
|
);
|
|
290
341
|
markDaemonReady();
|
|
@@ -13,7 +13,7 @@ import { extractToken } from '../mcp/request-identity.js';
|
|
|
13
13
|
import { publicBaseOrDefault } from './attach-serve.js';
|
|
14
14
|
import { AGENT_CODE_RE, takeAgentCode } from './agent-pair.js';
|
|
15
15
|
import { relayServersJson } from './connector-json.js';
|
|
16
|
-
import { sessionTtlFromEnv } from './
|
|
16
|
+
import { sessionTtlFromEnv } from './session-config.js';
|
|
17
17
|
import { signAgentToken } from './session.js';
|
|
18
18
|
import type { ConnectorApiDeps } from './connector-api.js';
|
|
19
19
|
|
|
@@ -58,14 +58,19 @@ async function handleClaim(
|
|
|
58
58
|
});
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
|
-
const agent = await deps.agentConnectors(taken.
|
|
61
|
+
const agent = await deps.agentConnectors(taken.subject, taken.agentId);
|
|
62
62
|
const token = signAgentToken(
|
|
63
|
-
{
|
|
63
|
+
{ subject: taken.subject, agentId: agent.id },
|
|
64
64
|
secret,
|
|
65
65
|
{ ttlSec: sessionTtlFromEnv() },
|
|
66
66
|
);
|
|
67
|
-
log.info({
|
|
68
|
-
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
|
+
});
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
async function handleRead(
|
|
@@ -80,9 +85,13 @@ async function handleRead(
|
|
|
80
85
|
return;
|
|
81
86
|
}
|
|
82
87
|
await assertLease(who, deps.fenceRuntime);
|
|
83
|
-
const agent = await deps.agentConnectors(who.
|
|
88
|
+
const agent = await deps.agentConnectors(who.subject, who.agentId);
|
|
84
89
|
if (path === SESSION_PATH) {
|
|
85
|
-
sendJson(req, res, 200, {
|
|
90
|
+
sendJson(req, res, 200, {
|
|
91
|
+
email: who.subject,
|
|
92
|
+
subject: who.subject,
|
|
93
|
+
agent: agent.name,
|
|
94
|
+
});
|
|
86
95
|
return;
|
|
87
96
|
}
|
|
88
97
|
const entries = await deps.connectorNamesByIds(agent.connectorIds);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
|
|
3
|
+
const uiBase = (): string => process.env.METRO_UI_URL ?? 'https://metro.box';
|
|
4
|
+
|
|
5
|
+
export const connectLink = (base: string): string =>
|
|
6
|
+
`${uiBase()}/#/connect/${encodeURIComponent(base)}`;
|
|
7
|
+
|
|
8
|
+
export function localConnectHint(port: number): string {
|
|
9
|
+
const here = `http://127.0.0.1:${String(port)}`;
|
|
10
|
+
const forward = `ssh -L ${String(port)}:127.0.0.1:${String(port)} ${hostname()}`;
|
|
11
|
+
return [
|
|
12
|
+
'Manage this machine from the web UI:',
|
|
13
|
+
'',
|
|
14
|
+
` ${connectLink(here)}`,
|
|
15
|
+
'',
|
|
16
|
+
`From another computer, forward the port first: ${forward}`,
|
|
17
|
+
'',
|
|
18
|
+
].join('\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function publicConnectHint(url: string): string {
|
|
22
|
+
return [
|
|
23
|
+
'Manage this machine from the web UI, from anywhere:',
|
|
24
|
+
'',
|
|
25
|
+
` ${connectLink(url)}`,
|
|
26
|
+
'',
|
|
27
|
+
'Sign in right away: the first wallet to sign in owns this machine.',
|
|
28
|
+
'',
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const tunnelPendingHint = (): string =>
|
|
33
|
+
'Bringing up a public address through a Cloudflare quick tunnel…\n';
|
|
@@ -35,23 +35,23 @@ const asText = (value: unknown): string =>
|
|
|
35
35
|
typeof value === 'string' ? value : '';
|
|
36
36
|
|
|
37
37
|
export interface ConnectorApiDeps extends OAuthRouteDeps {
|
|
38
|
-
listConnectors: (
|
|
38
|
+
listConnectors: (subject: string, project: string) => Promise<Connector[]>;
|
|
39
39
|
connectorNamesByIds: (ids: string[]) => Promise<RelayServerEntry[]>;
|
|
40
|
-
agentConnectors: (
|
|
40
|
+
agentConnectors: (subject: string, agentId: string) => Promise<AgentConnectors>;
|
|
41
41
|
fenceRuntime: Fence;
|
|
42
42
|
createConnector: (
|
|
43
|
-
|
|
43
|
+
subject: string,
|
|
44
44
|
project: string,
|
|
45
45
|
input: ConnectorInput,
|
|
46
46
|
) => Promise<Connector>;
|
|
47
|
-
verifyConnector: (
|
|
48
|
-
disconnectConnector: (
|
|
47
|
+
verifyConnector: (subject: string, id: string) => Promise<ConnectorCheck>;
|
|
48
|
+
disconnectConnector: (subject: string, id: string) => Promise<Connector>;
|
|
49
49
|
renameConnector: (
|
|
50
|
-
|
|
50
|
+
subject: string,
|
|
51
51
|
id: string,
|
|
52
52
|
name: string,
|
|
53
53
|
) => Promise<Connector>;
|
|
54
|
-
deleteConnector: (
|
|
54
|
+
deleteConnector: (subject: string, id: string) => Promise<DeletedConnector>;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
type Routable =
|
|
@@ -115,7 +115,7 @@ async function handleList(
|
|
|
115
115
|
sendJson(req, res, 400, { error: 'a project is required' });
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
|
-
const rows = await deps.listConnectors(session.
|
|
118
|
+
const rows = await deps.listConnectors(session.subject, project);
|
|
119
119
|
sendJson(req, res, 200, {
|
|
120
120
|
connectors: rows.map((row) => connectorPayload(row)),
|
|
121
121
|
});
|
|
@@ -135,7 +135,7 @@ async function handleCreate(
|
|
|
135
135
|
}
|
|
136
136
|
const offered = asText(bodyField(body, 'value')).trim() !== '';
|
|
137
137
|
try {
|
|
138
|
-
const created = await deps.createConnector(session.
|
|
138
|
+
const created = await deps.createConnector(session.subject, project, {
|
|
139
139
|
name: bodyField(body, 'name'),
|
|
140
140
|
url: bodyField(body, 'url'),
|
|
141
141
|
header: bodyField(body, 'header'),
|
|
@@ -161,7 +161,7 @@ async function handleVerify(
|
|
|
161
161
|
session: ApiSession,
|
|
162
162
|
id: string,
|
|
163
163
|
): Promise<void> {
|
|
164
|
-
const check = await deps.verifyConnector(session.
|
|
164
|
+
const check = await deps.verifyConnector(session.subject, id);
|
|
165
165
|
log.info(
|
|
166
166
|
{ id: check.id, name: check.name, ok: check.ok },
|
|
167
167
|
'connector-api: re-verified connector',
|
|
@@ -176,7 +176,7 @@ async function handleDisconnect(
|
|
|
176
176
|
session: ApiSession,
|
|
177
177
|
id: string,
|
|
178
178
|
): Promise<void> {
|
|
179
|
-
const row = await deps.disconnectConnector(session.
|
|
179
|
+
const row = await deps.disconnectConnector(session.subject, id);
|
|
180
180
|
log.info(
|
|
181
181
|
{ id: row.id, name: row.name },
|
|
182
182
|
'connector-api: signed the connector out',
|
|
@@ -191,7 +191,7 @@ async function handleDelete(
|
|
|
191
191
|
session: ApiSession,
|
|
192
192
|
id: string,
|
|
193
193
|
): Promise<void> {
|
|
194
|
-
const gone = await deps.deleteConnector(session.
|
|
194
|
+
const gone = await deps.deleteConnector(session.subject, id);
|
|
195
195
|
log.info(
|
|
196
196
|
{ id: gone.id, name: gone.name },
|
|
197
197
|
'connector-api: deleted connector',
|
|
@@ -210,7 +210,7 @@ async function handleConnector(
|
|
|
210
210
|
await handleDelete(req, res, deps, session, id);
|
|
211
211
|
return;
|
|
212
212
|
}
|
|
213
|
-
const row = await deps.getConnector(session.
|
|
213
|
+
const row = await deps.getConnector(session.subject, id);
|
|
214
214
|
sendJson(req, res, 200, connectorPayload(row, true));
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -226,7 +226,7 @@ async function handleRename(
|
|
|
226
226
|
sendJson(req, res, 400, { error: 'name is required' });
|
|
227
227
|
return;
|
|
228
228
|
}
|
|
229
|
-
const row = await deps.renameConnector(session.
|
|
229
|
+
const row = await deps.renameConnector(session.subject, id, name);
|
|
230
230
|
log.info({ id: row.id, name: row.name }, 'connector-api: renamed connector');
|
|
231
231
|
sendJson(req, res, 200, connectorPayload(row));
|
|
232
232
|
}
|
|
@@ -24,16 +24,16 @@ export function hostOf(url: string): string {
|
|
|
24
24
|
|
|
25
25
|
export interface OAuthRouteDeps {
|
|
26
26
|
createPendingConnector: (
|
|
27
|
-
|
|
27
|
+
subject: string,
|
|
28
28
|
project: string,
|
|
29
29
|
input: { name: unknown; url: unknown },
|
|
30
30
|
) => Promise<Connector>;
|
|
31
31
|
reconnectConnector: (
|
|
32
|
-
|
|
32
|
+
subject: string,
|
|
33
33
|
id: string,
|
|
34
34
|
auth: OAuthAuth,
|
|
35
35
|
) => Promise<Connector>;
|
|
36
|
-
getConnector: (
|
|
36
|
+
getConnector: (subject: string, id: string) => Promise<Connector>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export async function startOAuth(
|
|
@@ -46,12 +46,12 @@ export async function startOAuth(
|
|
|
46
46
|
payload: (row: Connector) => Record<string, unknown>,
|
|
47
47
|
): Promise<void> {
|
|
48
48
|
const url = parseConnectorUrl(bodyField(body, 'url'));
|
|
49
|
-
const row = await deps.createPendingConnector(session.
|
|
49
|
+
const row = await deps.createPendingConnector(session.subject, project, {
|
|
50
50
|
name: bodyField(body, 'name'),
|
|
51
51
|
url: bodyField(body, 'url'),
|
|
52
52
|
});
|
|
53
53
|
const authorize = await beginOAuth({
|
|
54
|
-
|
|
54
|
+
subject: session.subject,
|
|
55
55
|
name: row.name,
|
|
56
56
|
url,
|
|
57
57
|
returnTo: asText(bodyField(body, 'returnTo')),
|
|
@@ -71,11 +71,11 @@ export async function handleConnect(
|
|
|
71
71
|
session: ApiSession,
|
|
72
72
|
id: string,
|
|
73
73
|
): Promise<void> {
|
|
74
|
-
const row = await deps.getConnector(session.
|
|
74
|
+
const row = await deps.getConnector(session.subject, id);
|
|
75
75
|
const url = parseConnectorUrl(row.url);
|
|
76
76
|
const body = await readJsonBody(req);
|
|
77
77
|
const authorize = await beginOAuth({
|
|
78
|
-
|
|
78
|
+
subject: session.subject,
|
|
79
79
|
name: row.name,
|
|
80
80
|
url,
|
|
81
81
|
returnTo: asText(bodyField(body, 'returnTo')),
|
|
@@ -110,7 +110,7 @@ async function saveOAuth(
|
|
|
110
110
|
entry: PendingAuth,
|
|
111
111
|
auth: OAuthAuth,
|
|
112
112
|
): Promise<Connector> {
|
|
113
|
-
return deps.reconnectConnector(entry.
|
|
113
|
+
return deps.reconnectConnector(entry.subject, entry.connectorId, auth);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
async function settleCallback(
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type OAuthAuth,
|
|
5
5
|
type OAuthTokens,
|
|
6
6
|
} from './connector-verify.js';
|
|
7
|
-
import { validateReturnTo } from './
|
|
7
|
+
import { validateReturnTo } from './session-config.js';
|
|
8
8
|
import {
|
|
9
9
|
authorizeUrl,
|
|
10
10
|
exchangeCode,
|
|
@@ -32,7 +32,7 @@ const resourceOf = (url: URL): string => url.toString();
|
|
|
32
32
|
|
|
33
33
|
export interface BeginInput {
|
|
34
34
|
connectorId: string;
|
|
35
|
-
|
|
35
|
+
subject: string;
|
|
36
36
|
name: string;
|
|
37
37
|
url: URL;
|
|
38
38
|
returnTo: string;
|
|
@@ -47,7 +47,7 @@ export async function beginOAuth(input: BeginInput): Promise<string> {
|
|
|
47
47
|
const verifier = newVerifier();
|
|
48
48
|
const resource = resourceOf(input.url);
|
|
49
49
|
const state = startPending({
|
|
50
|
-
|
|
50
|
+
subject: input.subject,
|
|
51
51
|
name: input.name,
|
|
52
52
|
url: input.url.toString(),
|
|
53
53
|
resource,
|
|
@@ -27,7 +27,8 @@ import {
|
|
|
27
27
|
} from './tunnel.js';
|
|
28
28
|
import { attachmentEventUrl, handleAttachRequest } from './attach-serve.js';
|
|
29
29
|
import { webhookEntry } from '@metro-labs/webhook';
|
|
30
|
-
import {
|
|
30
|
+
import { handleSiweAuthRequest } from './siwe-routes.js';
|
|
31
|
+
import { handleModeRequest } from './mode-api.js';
|
|
31
32
|
import { handleUploadRequest } from './upload-api.js';
|
|
32
33
|
import {
|
|
33
34
|
handleMonitorRequest,
|
|
@@ -323,6 +324,15 @@ async function handleWebhookRoute(
|
|
|
323
324
|
return true;
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
function handleSignInRoutes(
|
|
328
|
+
req: IncomingMessage,
|
|
329
|
+
res: ServerResponse,
|
|
330
|
+
apis: SessionApis,
|
|
331
|
+
): boolean {
|
|
332
|
+
if (handleSiweAuthRequest(req, res, apis.siwe)) return true;
|
|
333
|
+
return apis.mode !== undefined && handleModeRequest(req, res, apis.mode);
|
|
334
|
+
}
|
|
335
|
+
|
|
326
336
|
async function handlePreMcpRoutes(
|
|
327
337
|
req: IncomingMessage,
|
|
328
338
|
res: ServerResponse,
|
|
@@ -331,7 +341,7 @@ async function handlePreMcpRoutes(
|
|
|
331
341
|
monitorCall?: MonitorCall,
|
|
332
342
|
): Promise<boolean> {
|
|
333
343
|
if (handleHealth(req, res)) return true;
|
|
334
|
-
if (
|
|
344
|
+
if (handleSignInRoutes(req, res, apis)) return true;
|
|
335
345
|
if (handleSessionApis(req, res, apis)) return true;
|
|
336
346
|
if (handleUploadRequest(req, res)) return true;
|
|
337
347
|
if (handleAttachRequest(req, res)) return true;
|
|
@@ -71,9 +71,16 @@ export function daemonSelf(): Line {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export function selfLine(): Line | null {
|
|
74
|
-
if (process.env.CLAUDECODE)
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
if (!process.env.CLAUDECODE) return null;
|
|
75
|
+
const s = claudeSessionId();
|
|
76
|
+
if (!s) return null;
|
|
77
|
+
try {
|
|
78
|
+
return Line.claude(claudeUserId(), s);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
log.warn(
|
|
81
|
+
{ reason: errMsg(e) },
|
|
82
|
+
'could not resolve a Claude account id; relaying with no self line',
|
|
83
|
+
);
|
|
84
|
+
return null;
|
|
77
85
|
}
|
|
78
|
-
return null;
|
|
79
86
|
}
|