@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
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
readJsonBody,
|
|
10
10
|
sendJson,
|
|
11
11
|
type ApiSession,
|
|
12
|
+
type Fence,
|
|
12
13
|
} from './api-http.js';
|
|
13
14
|
import { parseId } from '../db/ids.js';
|
|
14
|
-
import type {
|
|
15
|
+
import type { AgentConnectors } from '../db/agent-connectors.js';
|
|
15
16
|
import type { RelayServerEntry } from './connector-json.js';
|
|
16
17
|
import {
|
|
17
18
|
handleCallback,
|
|
@@ -34,50 +35,23 @@ const asText = (value: unknown): string =>
|
|
|
34
35
|
typeof value === 'string' ? value : '';
|
|
35
36
|
|
|
36
37
|
export interface ConnectorApiDeps extends OAuthRouteDeps {
|
|
37
|
-
listConnectors: (
|
|
38
|
+
listConnectors: (subject: string, project: string) => Promise<Connector[]>;
|
|
38
39
|
connectorNamesByIds: (ids: string[]) => Promise<RelayServerEntry[]>;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
project: string,
|
|
42
|
-
) => Promise<ConnectorCollectionRow[]>;
|
|
43
|
-
getCollection: (email: string, id: string) => Promise<ConnectorCollectionRow>;
|
|
44
|
-
createCollection: (
|
|
45
|
-
email: string,
|
|
46
|
-
project: string,
|
|
47
|
-
name: string,
|
|
48
|
-
) => Promise<ConnectorCollectionRow>;
|
|
49
|
-
renameCollection: (
|
|
50
|
-
email: string,
|
|
51
|
-
id: string,
|
|
52
|
-
name: string,
|
|
53
|
-
) => Promise<ConnectorCollectionRow>;
|
|
54
|
-
deleteCollection: (
|
|
55
|
-
email: string,
|
|
56
|
-
id: string,
|
|
57
|
-
) => Promise<{ id: string; name: string }>;
|
|
58
|
-
addToCollection: (
|
|
59
|
-
email: string,
|
|
60
|
-
id: string,
|
|
61
|
-
connectorId: string,
|
|
62
|
-
) => Promise<ConnectorCollectionRow>;
|
|
63
|
-
removeFromCollection: (
|
|
64
|
-
email: string,
|
|
65
|
-
id: string,
|
|
66
|
-
connectorId: string,
|
|
67
|
-
) => Promise<ConnectorCollectionRow>;
|
|
40
|
+
agentConnectors: (subject: string, agentId: string) => Promise<AgentConnectors>;
|
|
41
|
+
fenceRuntime: Fence;
|
|
68
42
|
createConnector: (
|
|
69
|
-
|
|
43
|
+
subject: string,
|
|
70
44
|
project: string,
|
|
71
45
|
input: ConnectorInput,
|
|
72
46
|
) => Promise<Connector>;
|
|
73
|
-
verifyConnector: (
|
|
74
|
-
disconnectConnector: (
|
|
47
|
+
verifyConnector: (subject: string, id: string) => Promise<ConnectorCheck>;
|
|
48
|
+
disconnectConnector: (subject: string, id: string) => Promise<Connector>;
|
|
75
49
|
renameConnector: (
|
|
76
|
-
|
|
50
|
+
subject: string,
|
|
77
51
|
id: string,
|
|
78
52
|
name: string,
|
|
79
53
|
) => Promise<Connector>;
|
|
80
|
-
deleteConnector: (
|
|
54
|
+
deleteConnector: (subject: string, id: string) => Promise<DeletedConnector>;
|
|
81
55
|
}
|
|
82
56
|
|
|
83
57
|
type Routable =
|
|
@@ -141,7 +115,7 @@ async function handleList(
|
|
|
141
115
|
sendJson(req, res, 400, { error: 'a project is required' });
|
|
142
116
|
return;
|
|
143
117
|
}
|
|
144
|
-
const rows = await deps.listConnectors(session.
|
|
118
|
+
const rows = await deps.listConnectors(session.subject, project);
|
|
145
119
|
sendJson(req, res, 200, {
|
|
146
120
|
connectors: rows.map((row) => connectorPayload(row)),
|
|
147
121
|
});
|
|
@@ -161,7 +135,7 @@ async function handleCreate(
|
|
|
161
135
|
}
|
|
162
136
|
const offered = asText(bodyField(body, 'value')).trim() !== '';
|
|
163
137
|
try {
|
|
164
|
-
const created = await deps.createConnector(session.
|
|
138
|
+
const created = await deps.createConnector(session.subject, project, {
|
|
165
139
|
name: bodyField(body, 'name'),
|
|
166
140
|
url: bodyField(body, 'url'),
|
|
167
141
|
header: bodyField(body, 'header'),
|
|
@@ -187,7 +161,7 @@ async function handleVerify(
|
|
|
187
161
|
session: ApiSession,
|
|
188
162
|
id: string,
|
|
189
163
|
): Promise<void> {
|
|
190
|
-
const check = await deps.verifyConnector(session.
|
|
164
|
+
const check = await deps.verifyConnector(session.subject, id);
|
|
191
165
|
log.info(
|
|
192
166
|
{ id: check.id, name: check.name, ok: check.ok },
|
|
193
167
|
'connector-api: re-verified connector',
|
|
@@ -202,7 +176,7 @@ async function handleDisconnect(
|
|
|
202
176
|
session: ApiSession,
|
|
203
177
|
id: string,
|
|
204
178
|
): Promise<void> {
|
|
205
|
-
const row = await deps.disconnectConnector(session.
|
|
179
|
+
const row = await deps.disconnectConnector(session.subject, id);
|
|
206
180
|
log.info(
|
|
207
181
|
{ id: row.id, name: row.name },
|
|
208
182
|
'connector-api: signed the connector out',
|
|
@@ -217,7 +191,7 @@ async function handleDelete(
|
|
|
217
191
|
session: ApiSession,
|
|
218
192
|
id: string,
|
|
219
193
|
): Promise<void> {
|
|
220
|
-
const gone = await deps.deleteConnector(session.
|
|
194
|
+
const gone = await deps.deleteConnector(session.subject, id);
|
|
221
195
|
log.info(
|
|
222
196
|
{ id: gone.id, name: gone.name },
|
|
223
197
|
'connector-api: deleted connector',
|
|
@@ -236,7 +210,7 @@ async function handleConnector(
|
|
|
236
210
|
await handleDelete(req, res, deps, session, id);
|
|
237
211
|
return;
|
|
238
212
|
}
|
|
239
|
-
const row = await deps.getConnector(session.
|
|
213
|
+
const row = await deps.getConnector(session.subject, id);
|
|
240
214
|
sendJson(req, res, 200, connectorPayload(row, true));
|
|
241
215
|
}
|
|
242
216
|
|
|
@@ -252,7 +226,7 @@ async function handleRename(
|
|
|
252
226
|
sendJson(req, res, 400, { error: 'name is required' });
|
|
253
227
|
return;
|
|
254
228
|
}
|
|
255
|
-
const row = await deps.renameConnector(session.
|
|
229
|
+
const row = await deps.renameConnector(session.subject, id, name);
|
|
256
230
|
log.info({ id: row.id, name: row.name }, 'connector-api: renamed connector');
|
|
257
231
|
sendJson(req, res, 200, connectorPayload(row));
|
|
258
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
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
import { ApiError } from './api-error.js';
|
|
3
|
+
import { errMsg, log } from './log.js';
|
|
4
|
+
import { AttachSessions } from './attach-session.js';
|
|
5
|
+
import type { AgentApiDeps } from './agent-api.js';
|
|
6
|
+
import { ATTACHABLE, type AccountApiDeps } from './account-api.js';
|
|
7
|
+
import type { AgentConnectorApiDeps } from './agent-connector-api.js';
|
|
8
|
+
import type { ProjectApiDeps } from './project-api.js';
|
|
9
|
+
import type { SessionApis } from './session-apis.js';
|
|
10
|
+
import type { ModeInfo } from './mode-api.js';
|
|
11
|
+
import {
|
|
12
|
+
claimLocalOwner,
|
|
13
|
+
LOCAL_PROJECT_ID,
|
|
14
|
+
localAttachAccount,
|
|
15
|
+
localCreateAgent,
|
|
16
|
+
localDeleteAgent,
|
|
17
|
+
localDetachAccount,
|
|
18
|
+
localListAgents,
|
|
19
|
+
localOwnedAgentOrThrow,
|
|
20
|
+
localOwner,
|
|
21
|
+
localResetAgentKey,
|
|
22
|
+
} from '../db/file-admin.js';
|
|
23
|
+
import { normalizeAddress } from '../db/users.js';
|
|
24
|
+
import type { Project } from '../db/projects.js';
|
|
25
|
+
import type { StationName } from '../db/schema.js';
|
|
26
|
+
|
|
27
|
+
export interface LocalModeDeps {
|
|
28
|
+
syncStations: (station: StationName) => Promise<void>;
|
|
29
|
+
closeAgentSession: (id: string) => Promise<boolean>;
|
|
30
|
+
gatherAccounts: AgentApiDeps['gatherAccounts'];
|
|
31
|
+
capabilities: AgentApiDeps['capabilities'];
|
|
32
|
+
liveness: AgentApiDeps['liveness'];
|
|
33
|
+
prepareAccount: AccountApiDeps['prepareAccount'];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const notHere = (what: string): Promise<never> =>
|
|
37
|
+
Promise.reject(
|
|
38
|
+
new ApiError(`${what} are managed on metro.box, not on a local daemon`, 400),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
function localProject(): Project {
|
|
42
|
+
return {
|
|
43
|
+
id: LOCAL_PROJECT_ID,
|
|
44
|
+
name: hostname(),
|
|
45
|
+
isDefault: true,
|
|
46
|
+
owner: true,
|
|
47
|
+
role: 'admin',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function attachSessions(deps: LocalModeDeps): AttachSessions {
|
|
52
|
+
return new AttachSessions({
|
|
53
|
+
authorize: async (owner) => {
|
|
54
|
+
await localOwnedAgentOrThrow(owner.subject, owner.agentId);
|
|
55
|
+
},
|
|
56
|
+
complete: async (owner, station, config) => {
|
|
57
|
+
const ref = await localAttachAccount(owner.subject, owner.agentId, station, config);
|
|
58
|
+
const activated = await deps.syncStations(station).then(
|
|
59
|
+
() => true,
|
|
60
|
+
(err: unknown) => {
|
|
61
|
+
log.warn(
|
|
62
|
+
{ station, err: errMsg(err) },
|
|
63
|
+
'attach-session: station reload failed, the change lands at the next boot',
|
|
64
|
+
);
|
|
65
|
+
return false;
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
return { accountId: ref.accountId, activated };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function agentApi(deps: LocalModeDeps): AgentApiDeps {
|
|
74
|
+
return {
|
|
75
|
+
attachSessions: attachSessions(deps),
|
|
76
|
+
listAgents: localListAgents,
|
|
77
|
+
createAgent: localCreateAgent,
|
|
78
|
+
deleteAgent: localDeleteAgent,
|
|
79
|
+
resetKey: async (subject, id) => {
|
|
80
|
+
const reset = await localResetAgentKey(subject, id);
|
|
81
|
+
const closed = await deps.closeAgentSession(id);
|
|
82
|
+
log.info({ agent: reset.name, id, sessionClosed: closed }, 'local: key rotated');
|
|
83
|
+
return reset;
|
|
84
|
+
},
|
|
85
|
+
gatherAccounts: deps.gatherAccounts,
|
|
86
|
+
capabilities: deps.capabilities,
|
|
87
|
+
attachable: ATTACHABLE.filter((s) => s !== 'webhook'),
|
|
88
|
+
liveness: deps.liveness,
|
|
89
|
+
releaseRuntime: () => notHere('runtime leases'),
|
|
90
|
+
runtimes: () => Promise.resolve(new Map()),
|
|
91
|
+
connectorIds: () => Promise.resolve(new Map()),
|
|
92
|
+
prepareAccount: deps.prepareAccount,
|
|
93
|
+
attachAccount: localAttachAccount,
|
|
94
|
+
detachAccount: localDetachAccount,
|
|
95
|
+
syncStations: deps.syncStations,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const projectApi: ProjectApiDeps = {
|
|
100
|
+
listProjects: (subject) => {
|
|
101
|
+
const owner = localOwner();
|
|
102
|
+
const mine = owner !== null && owner === normalizeAddress(subject);
|
|
103
|
+
return Promise.resolve(mine ? [localProject()] : []);
|
|
104
|
+
},
|
|
105
|
+
createProject: () => notHere('projects'),
|
|
106
|
+
renameProject: () => notHere('projects'),
|
|
107
|
+
deleteProject: () => notHere('projects'),
|
|
108
|
+
listMembers: (subject, id) => {
|
|
109
|
+
const owner = localOwner();
|
|
110
|
+
if (id !== LOCAL_PROJECT_ID || owner === null || owner !== normalizeAddress(subject))
|
|
111
|
+
return Promise.reject(new ApiError('no such project', 404));
|
|
112
|
+
return Promise.resolve([
|
|
113
|
+
{ id: 'owner', address: owner, email: null, role: 'admin', owner: true },
|
|
114
|
+
]);
|
|
115
|
+
},
|
|
116
|
+
addMember: () => notHere('members'),
|
|
117
|
+
setMemberRole: () => notHere('members'),
|
|
118
|
+
removeMember: () => notHere('members'),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const agentConnectorApi: AgentConnectorApiDeps = {
|
|
122
|
+
agentConnectors: async (subject, id) => {
|
|
123
|
+
const { agent } = await localOwnedAgentOrThrow(subject, id);
|
|
124
|
+
return { ...agent, connectorIds: [] };
|
|
125
|
+
},
|
|
126
|
+
addConnector: () => notHere('connectors'),
|
|
127
|
+
removeConnector: () => notHere('connectors'),
|
|
128
|
+
mintCode: () =>
|
|
129
|
+
Promise.reject(new ApiError('a local daemon has no pairing codes', 400)),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export function localConnectHint(port: number): string {
|
|
133
|
+
const ui = process.env.METRO_UI_URL ?? 'https://metro.box';
|
|
134
|
+
const here = `http://127.0.0.1:${String(port)}`;
|
|
135
|
+
const forward = `ssh -L ${String(port)}:127.0.0.1:${String(port)} ${hostname()}`;
|
|
136
|
+
return [
|
|
137
|
+
'Manage this machine from the web UI:',
|
|
138
|
+
'',
|
|
139
|
+
` ${ui}/#/connect/${encodeURIComponent(here)}`,
|
|
140
|
+
'',
|
|
141
|
+
`From another computer, forward the port first: ${forward}`,
|
|
142
|
+
'',
|
|
143
|
+
].join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function localModeInfo(): ModeInfo {
|
|
147
|
+
return { mode: 'local', owner: localOwner(), project: LOCAL_PROJECT_ID };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function localSessionApis(deps: LocalModeDeps): SessionApis {
|
|
151
|
+
return {
|
|
152
|
+
agentApi: agentApi(deps),
|
|
153
|
+
agentConnectorApi,
|
|
154
|
+
projectApi,
|
|
155
|
+
siwe: { ensureUser: claimLocalOwner },
|
|
156
|
+
mode: localModeInfo,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { agentsDir } from '../db/file-source.js';
|
|
5
|
+
import { ensureSecureDir, writeSecure } from './secure-fs.js';
|
|
6
|
+
|
|
7
|
+
const SECRET_FILE = '.session-secret';
|
|
8
|
+
|
|
9
|
+
export function ensureLocalSessionSecret(dir = agentsDir()): string {
|
|
10
|
+
const configured = process.env.METRO_SESSION_SECRET?.trim() ?? '';
|
|
11
|
+
if (configured !== '') return configured;
|
|
12
|
+
const path = join(dir, SECRET_FILE);
|
|
13
|
+
let secret = existsSync(path) ? readFileSync(path, 'utf8').trim() : '';
|
|
14
|
+
if (secret === '') {
|
|
15
|
+
secret = randomBytes(32).toString('base64url');
|
|
16
|
+
ensureSecureDir(dir);
|
|
17
|
+
writeSecure(path, `${secret}\n`);
|
|
18
|
+
}
|
|
19
|
+
process.env.METRO_SESSION_SECRET = secret;
|
|
20
|
+
return secret;
|
|
21
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { cors, sendJson } from './api-http.js';
|
|
3
|
+
|
|
4
|
+
const PATH = '/api/mode';
|
|
5
|
+
|
|
6
|
+
export type DaemonMode = 'hosted' | 'linked' | 'local';
|
|
7
|
+
|
|
8
|
+
export interface ModeInfo {
|
|
9
|
+
mode: DaemonMode;
|
|
10
|
+
owner: string | null;
|
|
11
|
+
project: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function handleModeRequest(
|
|
15
|
+
req: IncomingMessage,
|
|
16
|
+
res: ServerResponse,
|
|
17
|
+
info: () => ModeInfo,
|
|
18
|
+
): boolean {
|
|
19
|
+
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
20
|
+
if (path !== PATH) return false;
|
|
21
|
+
if (req.method === 'OPTIONS') {
|
|
22
|
+
res.writeHead(204, cors(req)).end();
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (req.method !== 'GET') {
|
|
26
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
sendJson(req, res, 200, info());
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from 'node:fs';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
13
14
|
import { log } from './log.js';
|
|
14
15
|
|
|
15
16
|
export const STATE_DIR =
|
|
@@ -23,6 +24,15 @@ export const CONFIG_ENV_FILE = join(CONFIG_DIR, '.env');
|
|
|
23
24
|
|
|
24
25
|
export const TRAINS_ENV_FILE = join(homedir(), '.metro', '.env');
|
|
25
26
|
|
|
27
|
+
export function trainsDir(): string {
|
|
28
|
+
const env = process.env.METRO_TRAINS_DIR?.trim();
|
|
29
|
+
if (env !== undefined && env !== '') return env;
|
|
30
|
+
return fileURLToPath(new URL('../../trains', import.meta.url));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const isLocalMode = (): boolean =>
|
|
34
|
+
process.env.METRO_MODE?.trim() === 'local';
|
|
35
|
+
|
|
26
36
|
export const envSources = (): readonly string[] => [
|
|
27
37
|
join(process.cwd(), '.env'),
|
|
28
38
|
TRAINS_ENV_FILE,
|
|
@@ -15,32 +15,32 @@ import type { Member, Project } from '../db/projects.js';
|
|
|
15
15
|
const PREFIX = '/api/projects';
|
|
16
16
|
|
|
17
17
|
export interface ProjectApiDeps {
|
|
18
|
-
listProjects: (
|
|
19
|
-
createProject: (
|
|
18
|
+
listProjects: (subject: string) => Promise<Project[]>;
|
|
19
|
+
createProject: (subject: string, name: unknown) => Promise<Project>;
|
|
20
20
|
renameProject: (
|
|
21
|
-
|
|
21
|
+
subject: string,
|
|
22
22
|
id: string,
|
|
23
23
|
name: unknown,
|
|
24
24
|
) => Promise<Project>;
|
|
25
25
|
deleteProject: (
|
|
26
|
-
|
|
26
|
+
subject: string,
|
|
27
27
|
id: string,
|
|
28
28
|
) => Promise<{ id: string; name: string }>;
|
|
29
|
-
listMembers: (
|
|
29
|
+
listMembers: (subject: string, id: string) => Promise<Member[]>;
|
|
30
30
|
addMember: (
|
|
31
|
-
|
|
31
|
+
subject: string,
|
|
32
32
|
id: string,
|
|
33
33
|
invited: unknown,
|
|
34
34
|
role: unknown,
|
|
35
35
|
) => Promise<Member[]>;
|
|
36
36
|
setMemberRole: (
|
|
37
|
-
|
|
37
|
+
subject: string,
|
|
38
38
|
id: string,
|
|
39
39
|
memberId: string,
|
|
40
40
|
role: unknown,
|
|
41
41
|
) => Promise<Member[]>;
|
|
42
42
|
removeMember: (
|
|
43
|
-
|
|
43
|
+
subject: string,
|
|
44
44
|
id: string,
|
|
45
45
|
memberId: string,
|
|
46
46
|
) => Promise<Member[]>;
|
|
@@ -98,11 +98,11 @@ async function handleIndex(
|
|
|
98
98
|
session: ApiSession,
|
|
99
99
|
): Promise<void> {
|
|
100
100
|
if (req.method === 'GET') {
|
|
101
|
-
sendJson(req, res, 200, { projects: await deps.listProjects(session.
|
|
101
|
+
sendJson(req, res, 200, { projects: await deps.listProjects(session.subject) });
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
const made = await deps.createProject(
|
|
105
|
-
session.
|
|
105
|
+
session.subject,
|
|
106
106
|
bodyField(await readJsonBody(req), 'name'),
|
|
107
107
|
);
|
|
108
108
|
log.info({ id: made.id, name: made.name }, 'project-api: created project');
|
|
@@ -117,14 +117,14 @@ async function handleMembers(
|
|
|
117
117
|
id: string,
|
|
118
118
|
): Promise<void> {
|
|
119
119
|
if (req.method === 'GET') {
|
|
120
|
-
sendJson(req, res, 200, { members: await deps.listMembers(session.
|
|
120
|
+
sendJson(req, res, 200, { members: await deps.listMembers(session.subject, id) });
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
const body = await readJsonBody(req);
|
|
124
124
|
const members = await deps.addMember(
|
|
125
|
-
session.
|
|
125
|
+
session.subject,
|
|
126
126
|
id,
|
|
127
|
-
bodyField(body, '
|
|
127
|
+
bodyField(body, 'address'),
|
|
128
128
|
bodyField(body, 'role'),
|
|
129
129
|
);
|
|
130
130
|
sendJson(req, res, 200, { members });
|
|
@@ -138,12 +138,12 @@ async function handleMember(
|
|
|
138
138
|
tgt: { id: string; memberId: string },
|
|
139
139
|
): Promise<void> {
|
|
140
140
|
if (req.method === 'DELETE') {
|
|
141
|
-
const members = await deps.removeMember(session.
|
|
141
|
+
const members = await deps.removeMember(session.subject, tgt.id, tgt.memberId);
|
|
142
142
|
sendJson(req, res, 200, { members });
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
145
|
const members = await deps.setMemberRole(
|
|
146
|
-
session.
|
|
146
|
+
session.subject,
|
|
147
147
|
tgt.id,
|
|
148
148
|
tgt.memberId,
|
|
149
149
|
bodyField(await readJsonBody(req), 'role'),
|
|
@@ -170,12 +170,12 @@ async function route(
|
|
|
170
170
|
res,
|
|
171
171
|
200,
|
|
172
172
|
await deps.renameProject(
|
|
173
|
-
session.
|
|
173
|
+
session.subject,
|
|
174
174
|
tgt.id,
|
|
175
175
|
bodyField(await readJsonBody(req), 'name'),
|
|
176
176
|
),
|
|
177
177
|
);
|
|
178
|
-
else sendJson(req, res, 200, await deps.deleteProject(session.
|
|
178
|
+
else sendJson(req, res, 200, await deps.deleteProject(session.subject, tgt.id));
|
|
179
179
|
} catch (err) {
|
|
180
180
|
apiFailure(req, res, err, 'project-api');
|
|
181
181
|
}
|