@stage-labs/metro 0.1.0-beta.69 → 0.1.0-beta.70
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/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/package.json +0 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +0 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +8 -15
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +7 -23
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +4 -11
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +0 -6
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +0 -8
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +0 -36
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +3 -9
- package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +1 -46
- package/runtime/runtime.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +0 -138
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.70",
|
|
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": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"scripts": {
|
|
33
33
|
"start": "bun src/server.ts",
|
|
34
34
|
"build": "tsc",
|
|
35
|
-
"db:generate": "drizzle-kit generate",
|
|
36
35
|
"db:migrate": "drizzle-kit migrate",
|
|
37
36
|
"test": "tsc --noEmit && DYLD_FALLBACK_LIBRARY_PATH=/usr/lib METRO_STATE_DIR=\"$(mktemp -d /tmp/metro-test.XXXXXX)\" bun test test/"
|
|
38
37
|
},
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ConnectorSummary } from '../db/connectors.js';
|
|
2
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
2
|
import { errMsg, log } from './log.js';
|
|
4
3
|
import {
|
|
@@ -12,7 +11,6 @@ import {
|
|
|
12
11
|
type ApiSession,
|
|
13
12
|
} from './api-http.js';
|
|
14
13
|
import { parseId } from '../db/ids.js';
|
|
15
|
-
import type { RelayServerEntry } from './connector-json.js';
|
|
16
14
|
import {
|
|
17
15
|
handleCallback,
|
|
18
16
|
handleConnect,
|
|
@@ -35,8 +33,6 @@ const asText = (value: unknown): string =>
|
|
|
35
33
|
|
|
36
34
|
export interface ConnectorApiDeps extends OAuthRouteDeps {
|
|
37
35
|
listConnectors: (subject: string, project: string) => Promise<Connector[]>;
|
|
38
|
-
connectorSummariesByIds: (ids: string[]) => Promise<ConnectorSummary[]>;
|
|
39
|
-
connectorNamesByIds: (ids: string[]) => Promise<RelayServerEntry[]>;
|
|
40
36
|
createConnector: (
|
|
41
37
|
subject: string,
|
|
42
38
|
project: string,
|
|
@@ -4,14 +4,12 @@ import { cors, sendJson, type AgentIdentity } from './api-http.js';
|
|
|
4
4
|
import { publicBaseOrDefault } from './attach-serve.js';
|
|
5
5
|
import { relayServersJson, type RelayServerEntry } from './connector-json.js';
|
|
6
6
|
import { agentIdForKey } from '../db/key-map.js';
|
|
7
|
-
import type { ConnectorSummary } from '../db/connectors.js';
|
|
8
7
|
|
|
9
|
-
const
|
|
8
|
+
const PATH = '/api/cli/mcp';
|
|
10
9
|
|
|
11
10
|
export interface LocalCliDeps {
|
|
12
11
|
agentName: (agentId: string) => string | null;
|
|
13
12
|
connectorEntries: (agentId: string) => Promise<RelayServerEntry[]>;
|
|
14
|
-
connectorSummaries: (agentId: string) => Promise<ConnectorSummary[]>;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
function keyOf(req: IncomingMessage): string {
|
|
@@ -27,25 +25,18 @@ export function keyIdentity(req: IncomingMessage): AgentIdentity | null {
|
|
|
27
25
|
return agentId === undefined ? null : { subject: 'agent-key', agentId };
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
async function answer(path: string, key: string, agentId: string, deps: LocalCliDeps): Promise<unknown> {
|
|
31
|
-
const agent = deps.agentName(agentId) ?? '';
|
|
32
|
-
if (path === `${PREFIX}/session`) return { subject: 'this machine', agent };
|
|
33
|
-
if (path === `${PREFIX}/connectors`) return { agent, connectors: await deps.connectorSummaries(agentId) };
|
|
34
|
-
return { json: relayServersJson(await deps.connectorEntries(agentId), publicBaseOrDefault(), key), agent };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
28
|
export function handleLocalCliRequest(
|
|
38
29
|
req: IncomingMessage,
|
|
39
30
|
res: ServerResponse,
|
|
40
31
|
deps: LocalCliDeps,
|
|
41
32
|
): boolean {
|
|
42
33
|
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
43
|
-
if (path !==
|
|
34
|
+
if (path !== PATH && !path.startsWith('/api/cli/')) return false;
|
|
44
35
|
if (req.method === 'OPTIONS') {
|
|
45
36
|
res.writeHead(204, cors(req)).end();
|
|
46
37
|
return true;
|
|
47
38
|
}
|
|
48
|
-
if (
|
|
39
|
+
if (path !== PATH) {
|
|
49
40
|
sendJson(req, res, 404, { error: 'not on a local daemon' });
|
|
50
41
|
return true;
|
|
51
42
|
}
|
|
@@ -58,9 +49,11 @@ export function handleLocalCliRequest(
|
|
|
58
49
|
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
59
50
|
return true;
|
|
60
51
|
}
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
|
|
52
|
+
deps
|
|
53
|
+
.connectorEntries(who.agentId)
|
|
54
|
+
.then((entries) => {
|
|
55
|
+
const agent = deps.agentName(who.agentId) ?? '';
|
|
56
|
+
sendJson(req, res, 200, { json: relayServersJson(entries, publicBaseOrDefault(), keyOf(req)), agent });
|
|
64
57
|
})
|
|
65
58
|
.catch((err: unknown) => {
|
|
66
59
|
log.warn({ err: errMsg(err) }, 'local cli: request failed');
|
|
@@ -3,7 +3,6 @@ import { errMsg, log } from './log.js';
|
|
|
3
3
|
import { AttachSessions } from './attach-session.js';
|
|
4
4
|
import type { AgentApiDeps } from './agent-api.js';
|
|
5
5
|
import { ATTACHABLE, type AccountApiDeps } from './account-api.js';
|
|
6
|
-
import type { AgentConnectorApiDeps } from './agent-connector-api.js';
|
|
7
6
|
import type { SessionApis } from './session-apis.js';
|
|
8
7
|
import type { ModeInfo } from './mode-api.js';
|
|
9
8
|
import { loadedAgentOf, type AgentBundle, type BundleApiDeps } from './bundle-api.js';
|
|
@@ -15,10 +14,7 @@ import type { RelayApiDeps } from './relay.js';
|
|
|
15
14
|
import {
|
|
16
15
|
readLocalConnectors,
|
|
17
16
|
localImportConnectors,
|
|
18
|
-
localAddConnector,
|
|
19
|
-
localAgentConnectors,
|
|
20
17
|
localConnectorNamesByIds,
|
|
21
|
-
localConnectorSummariesByIds,
|
|
22
18
|
localCreateConnector,
|
|
23
19
|
localCreatePendingConnector,
|
|
24
20
|
localDeleteConnector,
|
|
@@ -27,13 +23,11 @@ import {
|
|
|
27
23
|
localListConnectors,
|
|
28
24
|
localReconnectConnector,
|
|
29
25
|
localRelayTarget,
|
|
30
|
-
localRemoveConnector,
|
|
31
26
|
localRenameConnector,
|
|
32
27
|
localVerifyConnector,
|
|
33
28
|
} from '../db/local-connectors.js';
|
|
34
29
|
import {
|
|
35
30
|
assertLocalOwner,
|
|
36
|
-
connectorIdsOfLocalAgent,
|
|
37
31
|
LOCAL_PROJECT_ID,
|
|
38
32
|
localAttachAccount,
|
|
39
33
|
localCreateAgent,
|
|
@@ -106,20 +100,15 @@ function agentApi(deps: LocalModeDeps): AgentApiDeps {
|
|
|
106
100
|
};
|
|
107
101
|
}
|
|
108
102
|
|
|
103
|
+
const allConnectorIds = (): string[] => readLocalConnectors().map((c) => c.id);
|
|
104
|
+
|
|
109
105
|
function connectorIdsOfLocalAgents(ids: string[]): Promise<Map<string, string[]>> {
|
|
110
|
-
|
|
106
|
+
const all = allConnectorIds();
|
|
107
|
+
return Promise.resolve(new Map(ids.map((id) => [id, all] as const)));
|
|
111
108
|
}
|
|
112
109
|
|
|
113
|
-
const agentConnectorApi: AgentConnectorApiDeps = {
|
|
114
|
-
agentConnectors: localAgentConnectors,
|
|
115
|
-
addConnector: localAddConnector,
|
|
116
|
-
removeConnector: localRemoveConnector,
|
|
117
|
-
};
|
|
118
|
-
|
|
119
110
|
const connectorApi: ConnectorApiDeps = {
|
|
120
111
|
listConnectors: localListConnectors,
|
|
121
|
-
connectorSummariesByIds: (ids) => localConnectorSummariesByIds(ids),
|
|
122
|
-
connectorNamesByIds: (ids) => localConnectorNamesByIds(ids),
|
|
123
112
|
createConnector: localCreateConnector,
|
|
124
113
|
verifyConnector: localVerifyConnector,
|
|
125
114
|
disconnectConnector: localDisconnectConnector,
|
|
@@ -131,7 +120,7 @@ const connectorApi: ConnectorApiDeps = {
|
|
|
131
120
|
};
|
|
132
121
|
|
|
133
122
|
const relayApi: RelayApiDeps = {
|
|
134
|
-
target: (
|
|
123
|
+
target: (connectorId, force) => localRelayTarget(connectorId, force),
|
|
135
124
|
identify: keyIdentity,
|
|
136
125
|
};
|
|
137
126
|
|
|
@@ -143,8 +132,7 @@ function agentNameOf(agentId: string): string | null {
|
|
|
143
132
|
|
|
144
133
|
const localCli: LocalCliDeps = {
|
|
145
134
|
agentName: agentNameOf,
|
|
146
|
-
connectorEntries: (
|
|
147
|
-
connectorSummaries: (agentId) => localConnectorSummariesByIds(connectorIdsOfLocalAgent(agentId) ?? []),
|
|
135
|
+
connectorEntries: () => localConnectorNamesByIds(allConnectorIds()),
|
|
148
136
|
};
|
|
149
137
|
|
|
150
138
|
function bundleApi(deps: LocalModeDeps): BundleApiDeps {
|
|
@@ -152,13 +140,10 @@ function bundleApi(deps: LocalModeDeps): BundleApiDeps {
|
|
|
152
140
|
bundle: async (subject, agentId) => {
|
|
153
141
|
const { agent } = await localOwnedAgentOrThrow(subject, agentId);
|
|
154
142
|
const file = readLocalAgentFile(agentId);
|
|
155
|
-
const held = new Set(file.connectors);
|
|
156
143
|
const bundle: AgentBundle = {
|
|
157
144
|
version: 1,
|
|
158
145
|
agent: { id: file.id, name: file.name, key: file.key ?? '', stations: file.stations },
|
|
159
|
-
connectors: readLocalConnectors()
|
|
160
|
-
.filter((c) => held.has(c.id))
|
|
161
|
-
.map((c) => ({ id: c.id, name: c.name, url: c.url, transport: c.transport, config: { ...c.config } })),
|
|
146
|
+
connectors: readLocalConnectors().map((c) => ({ id: c.id, name: c.name, url: c.url, transport: c.transport, config: { ...c.config } })),
|
|
162
147
|
};
|
|
163
148
|
if (bundle.agent.key === '') throw new ApiError(`agent '${agent.name}' has no key to bundle`, 400);
|
|
164
149
|
return bundle;
|
|
@@ -184,7 +169,6 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
|
|
|
184
169
|
allowLocalConnectors(true);
|
|
185
170
|
return {
|
|
186
171
|
agentApi: agentApi(deps),
|
|
187
|
-
agentConnectorApi,
|
|
188
172
|
bundleApi: bundleApi(deps),
|
|
189
173
|
connectorApi,
|
|
190
174
|
relayApi,
|
|
@@ -5,11 +5,7 @@ import { ApiError } from './api-error.js';
|
|
|
5
5
|
import { errMsg, log } from './log.js';
|
|
6
6
|
|
|
7
7
|
export interface RelayApiDeps {
|
|
8
|
-
target: (
|
|
9
|
-
agentId: string,
|
|
10
|
-
connectorId: string,
|
|
11
|
-
force: boolean,
|
|
12
|
-
) => Promise<RelayTarget>;
|
|
8
|
+
target: (connectorId: string, force: boolean) => Promise<RelayTarget>;
|
|
13
9
|
identify: (req: IncomingMessage) => AgentIdentity | null;
|
|
14
10
|
}
|
|
15
11
|
|
|
@@ -197,18 +193,17 @@ type Exchanged =
|
|
|
197
193
|
|
|
198
194
|
async function exchange(
|
|
199
195
|
req: IncomingMessage,
|
|
200
|
-
agentId: string,
|
|
201
196
|
connectorId: string,
|
|
202
197
|
deps: RelayApiDeps,
|
|
203
198
|
body: Uint8Array<ArrayBuffer> | null,
|
|
204
199
|
signal: AbortSignal,
|
|
205
200
|
): Promise<Exchanged> {
|
|
206
|
-
const target = await deps.target(
|
|
201
|
+
const target = await deps.target(connectorId, false);
|
|
207
202
|
if (target.kind !== 'ok') return { kind: target.kind };
|
|
208
203
|
let upstream = await forward(req, target, body, signal);
|
|
209
204
|
if (!authFailed(upstream.status)) return { kind: 'response', upstream };
|
|
210
205
|
await upstream.body?.cancel();
|
|
211
|
-
const fresh = await deps.target(
|
|
206
|
+
const fresh = await deps.target(connectorId, true);
|
|
212
207
|
if (fresh.kind !== 'ok') return { kind: 'signin' };
|
|
213
208
|
upstream = await forward(req, fresh, body, signal);
|
|
214
209
|
if (!authFailed(upstream.status)) return { kind: 'response', upstream };
|
|
@@ -219,7 +214,6 @@ async function exchange(
|
|
|
219
214
|
async function relayExchange(
|
|
220
215
|
req: IncomingMessage,
|
|
221
216
|
res: ServerResponse,
|
|
222
|
-
agentId: string,
|
|
223
217
|
connectorId: string,
|
|
224
218
|
deps: RelayApiDeps,
|
|
225
219
|
): Promise<void> {
|
|
@@ -235,7 +229,6 @@ async function relayExchange(
|
|
|
235
229
|
const body = req.method === 'POST' ? await readCapped(req) : null;
|
|
236
230
|
const out = await exchange(
|
|
237
231
|
req,
|
|
238
|
-
agentId,
|
|
239
232
|
connectorId,
|
|
240
233
|
deps,
|
|
241
234
|
body,
|
|
@@ -268,7 +261,7 @@ function dispatch(
|
|
|
268
261
|
answer(res, 401, { error: 'unauthorized' });
|
|
269
262
|
return;
|
|
270
263
|
}
|
|
271
|
-
relayExchange(req, res,
|
|
264
|
+
relayExchange(req, res, connectorId, deps)
|
|
272
265
|
.catch((err: unknown) => {
|
|
273
266
|
if (err instanceof ApiError) {
|
|
274
267
|
answer(res, err.status, { error: err.message });
|
|
@@ -6,10 +6,6 @@ import {
|
|
|
6
6
|
type ConnectorApiDeps,
|
|
7
7
|
} from './connector-api.js';
|
|
8
8
|
import type { RelayApiDeps } from './relay.js';
|
|
9
|
-
import {
|
|
10
|
-
handleAgentConnectorRequest,
|
|
11
|
-
type AgentConnectorApiDeps,
|
|
12
|
-
} from './agent-connector-api.js';
|
|
13
9
|
import type { IdentityRouteDeps } from './identity-routes.js';
|
|
14
10
|
import { handleLocalCliRequest, type LocalCliDeps } from './local-cli-api.js';
|
|
15
11
|
import { handleClaudeRequest, type ClaudeApiDeps } from './claude-api.js';
|
|
@@ -24,7 +20,6 @@ import { handleTerminalRequest, type TerminalApiDeps } from './terminal-api.js';
|
|
|
24
20
|
|
|
25
21
|
export interface SessionApis {
|
|
26
22
|
agentApi?: AgentApiDeps;
|
|
27
|
-
agentConnectorApi?: AgentConnectorApiDeps;
|
|
28
23
|
bundleApi?: BundleApiDeps;
|
|
29
24
|
vaultApi?: VaultApiDeps;
|
|
30
25
|
serversApi?: ServersApiDeps;
|
|
@@ -60,7 +55,6 @@ export function handleSessionApis(
|
|
|
60
55
|
...when(apis.machineApi, (d) => handleMachineRequest(req, res, d)),
|
|
61
56
|
...when(apis.terminalApi, (d) => handleTerminalRequest(req, res, d)),
|
|
62
57
|
...when(apis.claudeApi, (d) => handleClaudeRequest(req, res, d)),
|
|
63
|
-
...when(apis.agentConnectorApi, (d) => handleAgentConnectorRequest(req, res, d)),
|
|
64
58
|
...when(apis.agentApi, (d) => handleAgentApiRequest(req, res, d)),
|
|
65
59
|
];
|
|
66
60
|
return routes.some((run) => run());
|
|
@@ -58,14 +58,6 @@ export function connectorFromRow(row: ConnectorLike): Connector {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export interface ConnectorSummary {
|
|
62
|
-
id: string;
|
|
63
|
-
name: string;
|
|
64
|
-
url: string;
|
|
65
|
-
transport: string;
|
|
66
|
-
signIn: ConnectorSignIn;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
61
|
export const UNVERIFIED = {
|
|
70
62
|
at: '',
|
|
71
63
|
server: '',
|
|
@@ -267,42 +267,6 @@ export function readLocalAgentFile(agentId: string, dir = agentsDir()): AgentFil
|
|
|
267
267
|
return found.file;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
export function connectorIdsOfLocalAgent(agentId: string, dir = agentsDir()): string[] | null {
|
|
271
|
-
const found = storedAgents(dir).find((s) => s.file.id === agentId);
|
|
272
|
-
return found === undefined ? null : found.file.connectors;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function localAgentConnectorIds(subject: string, agentId: string, dir = agentsDir()): string[] {
|
|
276
|
-
return ownedOrThrow(subject, agentId, dir).file.connectors;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function localSetAgentConnectors(
|
|
280
|
-
subject: string,
|
|
281
|
-
agentId: string,
|
|
282
|
-
ids: string[],
|
|
283
|
-
dir = agentsDir(),
|
|
284
|
-
): void {
|
|
285
|
-
const stored = ownedOrThrow(subject, agentId, dir);
|
|
286
|
-
stored.file.connectors = ids;
|
|
287
|
-
save(stored);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function localHoldEverywhere(connectorId: string, dir = agentsDir()): void {
|
|
291
|
-
for (const stored of storedAgents(dir)) {
|
|
292
|
-
if (stored.file.connectors.includes(connectorId)) continue;
|
|
293
|
-
stored.file.connectors = [...stored.file.connectors, connectorId];
|
|
294
|
-
save(stored);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export function localDropConnectorEverywhere(connectorId: string, dir = agentsDir()): void {
|
|
299
|
-
for (const stored of storedAgents(dir)) {
|
|
300
|
-
if (!stored.file.connectors.includes(connectorId)) continue;
|
|
301
|
-
stored.file.connectors = stored.file.connectors.filter((id) => id !== connectorId);
|
|
302
|
-
save(stored);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
270
|
function removeIfEmpty(folder: string): void {
|
|
307
271
|
try {
|
|
308
272
|
rmdirSync(folder);
|
|
@@ -70,14 +70,8 @@ function optionalMatch(
|
|
|
70
70
|
return value;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const list: unknown[] = Array.isArray(raw) ? raw.map((item: unknown) => item) : [];
|
|
76
|
-
const ids = list.filter((id): id is string => typeof id === 'string' && ID_RE.test(id));
|
|
77
|
-
if (!Array.isArray(raw) || ids.length !== list.length)
|
|
78
|
-
fail(path, 'connectors is not a list of 11-character ids');
|
|
79
|
-
return ids;
|
|
80
|
-
}
|
|
73
|
+
const connectorsOf = (raw: unknown): string[] =>
|
|
74
|
+
Array.isArray(raw) ? raw.filter((id): id is string => typeof id === 'string' && ID_RE.test(id)) : [];
|
|
81
75
|
|
|
82
76
|
export function parseAgentFile(raw: string, path: string): AgentFile {
|
|
83
77
|
let parsed: unknown;
|
|
@@ -108,7 +102,7 @@ export function parseAgentFile(raw: string, path: string): AgentFile {
|
|
|
108
102
|
key,
|
|
109
103
|
owner,
|
|
110
104
|
stations: stations.map((s, i) => stationOf(s, path, i)),
|
|
111
|
-
connectors: connectorsOf(parsed.connectors
|
|
105
|
+
connectors: connectorsOf(parsed.connectors),
|
|
112
106
|
};
|
|
113
107
|
}
|
|
114
108
|
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
connectorAuth,
|
|
17
17
|
connectorName,
|
|
18
18
|
readConfig,
|
|
19
|
-
signInState,
|
|
20
19
|
stamp,
|
|
21
20
|
type ConnectorConfig,
|
|
22
21
|
} from './connector-config.js';
|
|
@@ -32,21 +31,10 @@ import {
|
|
|
32
31
|
type Connector,
|
|
33
32
|
type ConnectorCheck,
|
|
34
33
|
type ConnectorInput,
|
|
35
|
-
type ConnectorSummary,
|
|
36
34
|
type DeletedConnector,
|
|
37
35
|
} from './connectors.js';
|
|
38
|
-
import type { AgentConnectors } from '../daemon/agent-connector-api.js';
|
|
39
36
|
import { agentsDir } from './file-source.js';
|
|
40
|
-
import {
|
|
41
|
-
assertLocalOwner,
|
|
42
|
-
connectorIdsOfLocalAgent,
|
|
43
|
-
localAgentConnectorIds,
|
|
44
|
-
localDropConnectorEverywhere,
|
|
45
|
-
localHoldEverywhere,
|
|
46
|
-
localOwnedAgentOrThrow,
|
|
47
|
-
localSetAgentConnectors,
|
|
48
|
-
LOCAL_PROJECT_ID,
|
|
49
|
-
} from './file-admin.js';
|
|
37
|
+
import { assertLocalOwner, LOCAL_PROJECT_ID } from './file-admin.js';
|
|
50
38
|
import { newId } from './ids.js';
|
|
51
39
|
import type { LoadedConnector } from './materialize.js';
|
|
52
40
|
|
|
@@ -130,7 +118,6 @@ export function localImportConnectors(rows: LoadedConnector[], dir = agentsDir()
|
|
|
130
118
|
}));
|
|
131
119
|
const ids = new Set(imported.map((r) => r.id));
|
|
132
120
|
writeRows(dir, [...current.filter((r) => !ids.has(r.id)), ...imported]);
|
|
133
|
-
for (const row of imported) localHoldEverywhere(row.id, dir);
|
|
134
121
|
return imported.length;
|
|
135
122
|
}
|
|
136
123
|
|
|
@@ -155,7 +142,6 @@ function insert(dir: string, name: string, url: URL, config: ConnectorConfig): C
|
|
|
155
142
|
assertNameFree(name, null, dir);
|
|
156
143
|
const row: LocalConnectorRow = { id: newId(), name, url: connectorUrlText(url), transport: 'http', config };
|
|
157
144
|
writeRows(dir, [...readLocalConnectors(dir), row]);
|
|
158
|
-
localHoldEverywhere(row.id, dir);
|
|
159
145
|
return connectorFromRow(row);
|
|
160
146
|
}
|
|
161
147
|
|
|
@@ -239,44 +225,16 @@ export async function localRenameConnector(subject: string, id: string, raw: str
|
|
|
239
225
|
export async function localDeleteConnector(subject: string, id: string, dir = agentsDir()): Promise<DeletedConnector> {
|
|
240
226
|
const row = rowOrThrow(subject, id, dir);
|
|
241
227
|
writeRows(dir, readLocalConnectors(dir).filter((r) => r.id !== id));
|
|
242
|
-
localDropConnectorEverywhere(id, dir);
|
|
243
228
|
return Promise.resolve({ id: row.id, name: row.name });
|
|
244
229
|
}
|
|
245
230
|
|
|
246
231
|
const byIds = (ids: string[], dir: string): LocalConnectorRow[] =>
|
|
247
232
|
readLocalConnectors(dir).filter((r) => ids.includes(r.id));
|
|
248
233
|
|
|
249
|
-
export async function localConnectorSummariesByIds(ids: string[], dir = agentsDir()): Promise<ConnectorSummary[]> {
|
|
250
|
-
return Promise.resolve(
|
|
251
|
-
byIds(ids, dir).map((r) => ({ id: r.id, name: r.name, url: r.url, transport: r.transport, signIn: signInState(r.config) })),
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
234
|
export async function localConnectorNamesByIds(ids: string[], dir = agentsDir()): Promise<{ id: string; name: string }[]> {
|
|
256
235
|
return Promise.resolve(byIds(ids, dir).map((r) => ({ id: r.id, name: r.name })));
|
|
257
236
|
}
|
|
258
237
|
|
|
259
|
-
export async function localAgentConnectors(subject: string, agentId: string, dir = agentsDir()): Promise<AgentConnectors> {
|
|
260
|
-
const { agent } = await localOwnedAgentOrThrow(subject, agentId, dir);
|
|
261
|
-
return { ...agent, connectorIds: localAgentConnectorIds(subject, agentId, dir) };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export async function localAddConnector(subject: string, agentId: string, connectorId: string, dir = agentsDir()): Promise<AgentConnectors> {
|
|
265
|
-
const { agent } = await localOwnedAgentOrThrow(subject, agentId, dir);
|
|
266
|
-
const row = readLocalConnectors(dir).find((r) => r.id === connectorId);
|
|
267
|
-
if (row === undefined) throw missing();
|
|
268
|
-
const held = localAgentConnectorIds(subject, agentId, dir);
|
|
269
|
-
if (!held.includes(connectorId)) localSetAgentConnectors(subject, agentId, [...held, connectorId], dir);
|
|
270
|
-
return { ...agent, connectorIds: localAgentConnectorIds(subject, agentId, dir) };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export async function localRemoveConnector(subject: string, agentId: string, connectorId: string, dir = agentsDir()): Promise<AgentConnectors> {
|
|
274
|
-
const { agent } = await localOwnedAgentOrThrow(subject, agentId, dir);
|
|
275
|
-
const held = localAgentConnectorIds(subject, agentId, dir);
|
|
276
|
-
localSetAgentConnectors(subject, agentId, held.filter((id) => id !== connectorId), dir);
|
|
277
|
-
return { ...agent, connectorIds: localAgentConnectorIds(subject, agentId, dir) };
|
|
278
|
-
}
|
|
279
|
-
|
|
280
238
|
const inflight = new Map<string, Promise<OAuthAuth>>();
|
|
281
239
|
|
|
282
240
|
function refreshOnce(row: LocalConnectorRow, auth: OAuthAuth, dir: string): Promise<OAuthAuth> {
|
|
@@ -306,13 +264,10 @@ async function oauthTarget(row: LocalConnectorRow, auth: OAuthAuth, force: boole
|
|
|
306
264
|
}
|
|
307
265
|
|
|
308
266
|
export async function localRelayTarget(
|
|
309
|
-
agentId: string,
|
|
310
267
|
connectorId: string,
|
|
311
268
|
force: boolean,
|
|
312
269
|
dir = agentsDir(),
|
|
313
270
|
): Promise<RelayTarget> {
|
|
314
|
-
const held = connectorIdsOfLocalAgent(agentId, dir);
|
|
315
|
-
if (!held?.includes(connectorId)) return { kind: 'missing' };
|
|
316
271
|
const row = readLocalConnectors(dir).find((r) => r.id === connectorId);
|
|
317
272
|
if (row === undefined) return { kind: 'missing' };
|
|
318
273
|
parseConnectorUrl(row.url);
|
package/runtime/runtime.json
CHANGED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import { errMsg, log } from './log.js';
|
|
3
|
-
import {
|
|
4
|
-
apiFailure,
|
|
5
|
-
apiSession,
|
|
6
|
-
bodyField,
|
|
7
|
-
cors,
|
|
8
|
-
readJsonBody,
|
|
9
|
-
sendJson,
|
|
10
|
-
type ApiSession,
|
|
11
|
-
} from './api-http.js';
|
|
12
|
-
import { parseId } from '../db/ids.js';
|
|
13
|
-
|
|
14
|
-
const PREFIX = '/api/agents';
|
|
15
|
-
|
|
16
|
-
export interface AgentConnectors {
|
|
17
|
-
id: string;
|
|
18
|
-
name: string;
|
|
19
|
-
connectorIds: string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface AgentConnectorApiDeps {
|
|
23
|
-
agentConnectors: (subject: string, agentId: string) => Promise<AgentConnectors>;
|
|
24
|
-
addConnector: (
|
|
25
|
-
subject: string,
|
|
26
|
-
agentId: string,
|
|
27
|
-
connectorId: string,
|
|
28
|
-
) => Promise<AgentConnectors>;
|
|
29
|
-
removeConnector: (
|
|
30
|
-
subject: string,
|
|
31
|
-
agentId: string,
|
|
32
|
-
connectorId: string,
|
|
33
|
-
) => Promise<AgentConnectors>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type Routable =
|
|
37
|
-
| { kind: 'connectors'; id: string }
|
|
38
|
-
| { kind: 'connector'; id: string; connectorId: string };
|
|
39
|
-
|
|
40
|
-
type Target = Routable | { kind: 'unknown' } | null;
|
|
41
|
-
|
|
42
|
-
const ALLOWED: Record<Routable['kind'], string[]> = {
|
|
43
|
-
connectors: ['GET', 'POST'],
|
|
44
|
-
connector: ['DELETE'],
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
function connectorTarget(id: string, rest: string[]): Target {
|
|
48
|
-
if (rest.length === 1) return { kind: 'connectors', id };
|
|
49
|
-
if (rest.length > 2) return { kind: 'unknown' };
|
|
50
|
-
const connectorId = parseId(rest[1] ?? '');
|
|
51
|
-
return connectorId === null
|
|
52
|
-
? { kind: 'unknown' }
|
|
53
|
-
: { kind: 'connector', id, connectorId };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function target(path: string): Target {
|
|
57
|
-
if (!path.startsWith(`${PREFIX}/`)) return null;
|
|
58
|
-
const segments = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
|
|
59
|
-
const id = parseId(segments[0] ?? '');
|
|
60
|
-
const head = segments[1];
|
|
61
|
-
if (id === null || head === undefined) return null;
|
|
62
|
-
if (head === 'connectors') return connectorTarget(id, segments.slice(1));
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function handleConnectors(
|
|
67
|
-
req: IncomingMessage,
|
|
68
|
-
res: ServerResponse,
|
|
69
|
-
deps: AgentConnectorApiDeps,
|
|
70
|
-
session: ApiSession,
|
|
71
|
-
id: string,
|
|
72
|
-
): Promise<void> {
|
|
73
|
-
if (req.method === 'GET') {
|
|
74
|
-
sendJson(req, res, 200, await deps.agentConnectors(session.subject, id));
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const connectorId = bodyField(await readJsonBody(req), 'connectorId');
|
|
78
|
-
if (typeof connectorId !== 'string' || parseId(connectorId) === null) {
|
|
79
|
-
sendJson(req, res, 400, { error: 'connectorId is required' });
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
sendJson(req, res, 200, await deps.addConnector(session.subject, id, connectorId));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function route(
|
|
86
|
-
req: IncomingMessage,
|
|
87
|
-
res: ServerResponse,
|
|
88
|
-
deps: AgentConnectorApiDeps,
|
|
89
|
-
session: ApiSession,
|
|
90
|
-
tgt: Routable,
|
|
91
|
-
): Promise<void> {
|
|
92
|
-
try {
|
|
93
|
-
if (tgt.kind === 'connectors') await handleConnectors(req, res, deps, session, tgt.id);
|
|
94
|
-
else
|
|
95
|
-
sendJson(
|
|
96
|
-
req,
|
|
97
|
-
res,
|
|
98
|
-
200,
|
|
99
|
-
await deps.removeConnector(session.subject, tgt.id, tgt.connectorId),
|
|
100
|
-
);
|
|
101
|
-
} catch (err) {
|
|
102
|
-
apiFailure(req, res, err, 'agent-api');
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function handleAgentConnectorRequest(
|
|
107
|
-
req: IncomingMessage,
|
|
108
|
-
res: ServerResponse,
|
|
109
|
-
deps: AgentConnectorApiDeps,
|
|
110
|
-
): boolean {
|
|
111
|
-
const tgt = target((req.url ?? '').split('?')[0] ?? '');
|
|
112
|
-
if (tgt === null) return false;
|
|
113
|
-
if (req.method === 'OPTIONS') {
|
|
114
|
-
res.writeHead(204, cors(req)).end();
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
if (tgt.kind === 'unknown') {
|
|
118
|
-
sendJson(req, res, 404, { error: 'no such connector' });
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
if (!ALLOWED[tgt.kind].includes(req.method ?? '')) {
|
|
122
|
-
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
apiSession(req)
|
|
126
|
-
.then((session) => {
|
|
127
|
-
if (!session) {
|
|
128
|
-
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
return route(req, res, deps, session, tgt);
|
|
132
|
-
})
|
|
133
|
-
.catch((err: unknown) => {
|
|
134
|
-
log.warn({ err: errMsg(err) }, 'agent-api: unhandled error');
|
|
135
|
-
if (!res.headersSent) sendJson(req, res, 500, { error: 'agent api failed' });
|
|
136
|
-
});
|
|
137
|
-
return true;
|
|
138
|
-
}
|