@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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { and, asc, eq, inArray, ne } from 'drizzle-orm';
|
|
2
|
+
import { getDb } from './client.js';
|
|
3
|
+
import { newId } from './ids.js';
|
|
4
|
+
import { isUniqueViolation } from './users.js';
|
|
5
|
+
import { ownedAgentOrThrow } from './agent-admin.js';
|
|
6
|
+
import { ConnectorError } from './connector-config.js';
|
|
7
|
+
import { agentConnectors, agents, connectors } from './schema.js';
|
|
8
|
+
|
|
9
|
+
export interface AgentConnectors {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
connectorIds: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const nameClash = (name: string, agent: string): ConnectorError =>
|
|
16
|
+
new ConnectorError(
|
|
17
|
+
`the agent '${agent}' already has a connector named '${name}'`,
|
|
18
|
+
409,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
async function collidingAgent(
|
|
22
|
+
agentIds: string[],
|
|
23
|
+
name: string,
|
|
24
|
+
exceptConnectorId: string,
|
|
25
|
+
): Promise<string | null> {
|
|
26
|
+
if (agentIds.length === 0) return null;
|
|
27
|
+
const rows = await getDb()
|
|
28
|
+
.select({ agent: agents.name })
|
|
29
|
+
.from(agentConnectors)
|
|
30
|
+
.innerJoin(connectors, eq(connectors.id, agentConnectors.connectorId))
|
|
31
|
+
.innerJoin(agents, eq(agents.id, agentConnectors.agentId))
|
|
32
|
+
.where(
|
|
33
|
+
and(
|
|
34
|
+
inArray(agentConnectors.agentId, agentIds),
|
|
35
|
+
eq(connectors.name, name),
|
|
36
|
+
ne(connectors.id, exceptConnectorId),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
.limit(1);
|
|
40
|
+
return rows[0]?.agent ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function assertRenameFreeOfClash(
|
|
44
|
+
connectorId: string,
|
|
45
|
+
name: string,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
const rows = await getDb()
|
|
48
|
+
.select({ agentId: agentConnectors.agentId })
|
|
49
|
+
.from(agentConnectors)
|
|
50
|
+
.where(eq(agentConnectors.connectorId, connectorId));
|
|
51
|
+
const clash = await collidingAgent(
|
|
52
|
+
rows.map((r) => r.agentId),
|
|
53
|
+
name,
|
|
54
|
+
connectorId,
|
|
55
|
+
);
|
|
56
|
+
if (clash !== null) throw nameClash(name, clash);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function connectorIdsOfAgent(agentId: string): Promise<string[]> {
|
|
60
|
+
const rows = await getDb()
|
|
61
|
+
.select({ connectorId: agentConnectors.connectorId })
|
|
62
|
+
.from(agentConnectors)
|
|
63
|
+
.where(eq(agentConnectors.agentId, agentId))
|
|
64
|
+
.orderBy(asc(agentConnectors.id));
|
|
65
|
+
return rows.map((r) => r.connectorId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function connectorIdsByAgent(
|
|
69
|
+
agentIds: string[],
|
|
70
|
+
): Promise<Map<string, string[]>> {
|
|
71
|
+
const out = new Map<string, string[]>();
|
|
72
|
+
if (agentIds.length === 0) return out;
|
|
73
|
+
const rows = await getDb()
|
|
74
|
+
.select({
|
|
75
|
+
agentId: agentConnectors.agentId,
|
|
76
|
+
connectorId: agentConnectors.connectorId,
|
|
77
|
+
})
|
|
78
|
+
.from(agentConnectors)
|
|
79
|
+
.where(inArray(agentConnectors.agentId, agentIds))
|
|
80
|
+
.orderBy(asc(agentConnectors.id));
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const list = out.get(row.agentId) ?? [];
|
|
83
|
+
list.push(row.connectorId);
|
|
84
|
+
out.set(row.agentId, list);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function agentConnectorsForUser(
|
|
90
|
+
subject: string,
|
|
91
|
+
agentId: string,
|
|
92
|
+
): Promise<AgentConnectors> {
|
|
93
|
+
const { agent } = await ownedAgentOrThrow(subject, agentId);
|
|
94
|
+
return { ...agent, connectorIds: await connectorIdsOfAgent(agent.id) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function connectorNameIn(
|
|
98
|
+
projectId: string,
|
|
99
|
+
connectorId: string,
|
|
100
|
+
): Promise<string | null> {
|
|
101
|
+
const rows = await getDb()
|
|
102
|
+
.select({ name: connectors.name })
|
|
103
|
+
.from(connectors)
|
|
104
|
+
.where(and(eq(connectors.id, connectorId), eq(connectors.projectId, projectId)));
|
|
105
|
+
return rows[0]?.name ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function addConnectorToAgentForUser(
|
|
109
|
+
subject: string,
|
|
110
|
+
agentId: string,
|
|
111
|
+
connectorId: string,
|
|
112
|
+
): Promise<AgentConnectors> {
|
|
113
|
+
const { agent, projectId } = await ownedAgentOrThrow(subject, agentId);
|
|
114
|
+
const name = await connectorNameIn(projectId, connectorId);
|
|
115
|
+
if (name === null) throw new ConnectorError('no such connector', 404);
|
|
116
|
+
const clash = await collidingAgent([agent.id], name, connectorId);
|
|
117
|
+
if (clash !== null) throw nameClash(name, clash);
|
|
118
|
+
try {
|
|
119
|
+
await getDb()
|
|
120
|
+
.insert(agentConnectors)
|
|
121
|
+
.values({ id: newId(), agentId: agent.id, connectorId });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (!isUniqueViolation(err)) throw err;
|
|
124
|
+
}
|
|
125
|
+
return { ...agent, connectorIds: await connectorIdsOfAgent(agent.id) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function removeConnectorFromAgentForUser(
|
|
129
|
+
subject: string,
|
|
130
|
+
agentId: string,
|
|
131
|
+
connectorId: string,
|
|
132
|
+
): Promise<AgentConnectors> {
|
|
133
|
+
const { agent } = await ownedAgentOrThrow(subject, agentId);
|
|
134
|
+
await getDb()
|
|
135
|
+
.delete(agentConnectors)
|
|
136
|
+
.where(
|
|
137
|
+
and(
|
|
138
|
+
eq(agentConnectors.agentId, agent.id),
|
|
139
|
+
eq(agentConnectors.connectorId, connectorId),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
return { ...agent, connectorIds: await connectorIdsOfAgent(agent.id) };
|
|
143
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { errMsg, log } from '../daemon/log.js';
|
|
9
9
|
import { readConfig, type ConnectorConfig } from './connector-config.js';
|
|
10
10
|
import { getDb } from './client.js';
|
|
11
|
-
import {
|
|
11
|
+
import { agentConnectors, connectors } from './schema.js';
|
|
12
12
|
|
|
13
13
|
export type RelayTarget =
|
|
14
14
|
| { kind: 'ok'; url: string; headers: Record<string, string> }
|
|
@@ -32,17 +32,17 @@ export function headerAuthHeaders(
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async function memberRow(
|
|
35
|
-
|
|
35
|
+
agentId: string,
|
|
36
36
|
connectorId: string,
|
|
37
37
|
): Promise<ConnectorRow | undefined> {
|
|
38
38
|
const rows = await getDb()
|
|
39
39
|
.select({ connector: connectors })
|
|
40
|
-
.from(
|
|
41
|
-
.innerJoin(connectors, eq(
|
|
40
|
+
.from(agentConnectors)
|
|
41
|
+
.innerJoin(connectors, eq(agentConnectors.connectorId, connectors.id))
|
|
42
42
|
.where(
|
|
43
43
|
and(
|
|
44
|
-
eq(
|
|
45
|
-
eq(
|
|
44
|
+
eq(agentConnectors.agentId, agentId),
|
|
45
|
+
eq(agentConnectors.connectorId, connectorId),
|
|
46
46
|
),
|
|
47
47
|
);
|
|
48
48
|
return rows[0]?.connector;
|
|
@@ -112,11 +112,11 @@ export function fixedTarget(
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export async function relayTarget(
|
|
115
|
-
|
|
115
|
+
agentId: string,
|
|
116
116
|
connectorId: string,
|
|
117
117
|
force: boolean,
|
|
118
118
|
): Promise<RelayTarget> {
|
|
119
|
-
const row = await memberRow(
|
|
119
|
+
const row = await memberRow(agentId, connectorId);
|
|
120
120
|
if (row === undefined) return { kind: 'missing' };
|
|
121
121
|
parseConnectorUrl(row.url);
|
|
122
122
|
const config = readConfig(row.config);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertRenameFreeOfClash } from './
|
|
1
|
+
import { assertRenameFreeOfClash } from './agent-connectors.js';
|
|
2
2
|
import { projectIdOrThrow } from './projects.js';
|
|
3
3
|
import { and, asc, eq, inArray } from 'drizzle-orm';
|
|
4
4
|
import {
|
|
@@ -80,7 +80,7 @@ const missing = (): ConnectorError =>
|
|
|
80
80
|
new ConnectorError('no such connector', 404);
|
|
81
81
|
|
|
82
82
|
async function ownedConnectorOrThrow(
|
|
83
|
-
|
|
83
|
+
subject: string,
|
|
84
84
|
id: string,
|
|
85
85
|
): Promise<ConnectorRow> {
|
|
86
86
|
const rows = await getDb()
|
|
@@ -90,7 +90,7 @@ async function ownedConnectorOrThrow(
|
|
|
90
90
|
const row = rows[0];
|
|
91
91
|
if (row === undefined) throw missing();
|
|
92
92
|
try {
|
|
93
|
-
await projectIdOrThrow(
|
|
93
|
+
await projectIdOrThrow(subject, row.projectId);
|
|
94
94
|
} catch {
|
|
95
95
|
throw missing();
|
|
96
96
|
}
|
|
@@ -112,10 +112,10 @@ async function saveConfig(
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
async function connectorRowsFor(
|
|
115
|
-
|
|
115
|
+
subject: string,
|
|
116
116
|
project: string,
|
|
117
117
|
): Promise<ConnectorRow[]> {
|
|
118
|
-
const projectId = await projectIdOrThrow(
|
|
118
|
+
const projectId = await projectIdOrThrow(subject, project);
|
|
119
119
|
return getDb()
|
|
120
120
|
.select()
|
|
121
121
|
.from(connectors)
|
|
@@ -123,11 +123,11 @@ async function connectorRowsFor(
|
|
|
123
123
|
.orderBy(asc(connectors.id));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
export async function
|
|
127
|
-
|
|
126
|
+
export async function listConnectorsForUser(
|
|
127
|
+
subject: string,
|
|
128
128
|
project: string,
|
|
129
129
|
): Promise<Connector[]> {
|
|
130
|
-
return (await connectorRowsFor(
|
|
130
|
+
return (await connectorRowsFor(subject, project)).map(toConnector);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
async function rowsByIds(ids: string[]): Promise<ConnectorRow[]> {
|
|
@@ -145,11 +145,11 @@ export async function connectorNamesByIds(
|
|
|
145
145
|
return (await rowsByIds(ids)).map((row) => ({ id: row.id, name: row.name }));
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
export async function
|
|
149
|
-
|
|
148
|
+
export async function getConnectorForUser(
|
|
149
|
+
subject: string,
|
|
150
150
|
id: string,
|
|
151
151
|
): Promise<Connector> {
|
|
152
|
-
return toConnector(await ownedConnectorOrThrow(
|
|
152
|
+
return toConnector(await ownedConnectorOrThrow(subject, id));
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
async function insertConnector(
|
|
@@ -185,14 +185,14 @@ const UNVERIFIED = {
|
|
|
185
185
|
catalog: [],
|
|
186
186
|
};
|
|
187
187
|
|
|
188
|
-
export async function
|
|
189
|
-
|
|
188
|
+
export async function createPendingConnectorForUser(
|
|
189
|
+
subject: string,
|
|
190
190
|
project: string,
|
|
191
191
|
input: { name: unknown; url: unknown },
|
|
192
192
|
): Promise<Connector> {
|
|
193
193
|
const name = connectorName(input.name);
|
|
194
194
|
const url = parseConnectorUrl(input.url);
|
|
195
|
-
const projectId = await projectIdOrThrow(
|
|
195
|
+
const projectId = await projectIdOrThrow(subject, project);
|
|
196
196
|
return insertConnector(projectId, name, url, {
|
|
197
197
|
auth: { kind: 'none' },
|
|
198
198
|
createdAt: new Date().toISOString(),
|
|
@@ -201,15 +201,15 @@ export async function createPendingConnectorForEmail(
|
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
export async function
|
|
205
|
-
|
|
204
|
+
export async function createConnectorForUser(
|
|
205
|
+
subject: string,
|
|
206
206
|
project: string,
|
|
207
207
|
input: ConnectorInput,
|
|
208
208
|
): Promise<Connector> {
|
|
209
209
|
const name = connectorName(input.name);
|
|
210
210
|
const url = parseConnectorUrl(input.url);
|
|
211
211
|
const auth = connectorAuth(input.header, input.value);
|
|
212
|
-
const projectId = await projectIdOrThrow(
|
|
212
|
+
const projectId = await projectIdOrThrow(subject, project);
|
|
213
213
|
const verified = stamp(await verifyRemoteMcp(url, auth));
|
|
214
214
|
return insertConnector(projectId, name, url, {
|
|
215
215
|
auth,
|
|
@@ -233,34 +233,34 @@ async function freshAuth(
|
|
|
233
233
|
return refreshOAuth(auth, resource);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
export async function
|
|
237
|
-
|
|
236
|
+
export async function reconnectConnectorForUser(
|
|
237
|
+
subject: string,
|
|
238
238
|
id: string,
|
|
239
239
|
auth: OAuthAuth,
|
|
240
240
|
): Promise<Connector> {
|
|
241
|
-
const row = await ownedConnectorOrThrow(
|
|
241
|
+
const row = await ownedConnectorOrThrow(subject, id);
|
|
242
242
|
const config = readConfig(row.config);
|
|
243
243
|
const url = parseConnectorUrl(row.url);
|
|
244
244
|
const verified = stamp(await verifyRemoteMcp(url, auth));
|
|
245
245
|
return saveConfig(row, { ...config, auth, verified, oauth: true });
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
export async function
|
|
249
|
-
|
|
248
|
+
export async function disconnectConnectorForUser(
|
|
249
|
+
subject: string,
|
|
250
250
|
id: string,
|
|
251
251
|
): Promise<Connector> {
|
|
252
|
-
const row = await ownedConnectorOrThrow(
|
|
252
|
+
const row = await ownedConnectorOrThrow(subject, id);
|
|
253
253
|
const config = readConfig(row.config);
|
|
254
254
|
if (config.auth.kind !== 'oauth')
|
|
255
255
|
throw new ConnectorError('that connector is not signed in', 400);
|
|
256
256
|
return saveConfig(row, { ...config, auth: { kind: 'none' }, oauth: true });
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
export async function
|
|
260
|
-
|
|
259
|
+
export async function verifyConnectorForUser(
|
|
260
|
+
subject: string,
|
|
261
261
|
id: string,
|
|
262
262
|
): Promise<ConnectorCheck> {
|
|
263
|
-
const row = await ownedConnectorOrThrow(
|
|
263
|
+
const row = await ownedConnectorOrThrow(subject, id);
|
|
264
264
|
const config = readConfig(row.config);
|
|
265
265
|
try {
|
|
266
266
|
const url = parseConnectorUrl(row.url);
|
|
@@ -275,13 +275,13 @@ export async function verifyConnectorForEmail(
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
export async function
|
|
279
|
-
|
|
278
|
+
export async function renameConnectorForUser(
|
|
279
|
+
subject: string,
|
|
280
280
|
id: string,
|
|
281
281
|
raw: string,
|
|
282
282
|
): Promise<Connector> {
|
|
283
283
|
const name = connectorName(raw);
|
|
284
|
-
const row = await ownedConnectorOrThrow(
|
|
284
|
+
const row = await ownedConnectorOrThrow(subject, id);
|
|
285
285
|
await assertRenameFreeOfClash(row.id, name);
|
|
286
286
|
const rows = await getDb()
|
|
287
287
|
.update(connectors)
|
|
@@ -293,11 +293,11 @@ export async function renameConnectorForEmail(
|
|
|
293
293
|
return toConnector(saved);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
export async function
|
|
297
|
-
|
|
296
|
+
export async function deleteConnectorForUser(
|
|
297
|
+
subject: string,
|
|
298
298
|
id: string,
|
|
299
299
|
): Promise<DeletedConnector> {
|
|
300
|
-
const row = await ownedConnectorOrThrow(
|
|
300
|
+
const row = await ownedConnectorOrThrow(subject, id);
|
|
301
301
|
const gone = await getDb()
|
|
302
302
|
.delete(connectors)
|
|
303
303
|
.where(eq(connectors.id, row.id))
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
openSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmdirSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { ApiError } from '../daemon/api-error.js';
|
|
12
|
+
import { ensureSecureDir, writeSecure } from '../daemon/secure-fs.js';
|
|
13
|
+
import {
|
|
14
|
+
AgentAdminError,
|
|
15
|
+
newApiKey,
|
|
16
|
+
normalizeAgentName,
|
|
17
|
+
type AgentSummary,
|
|
18
|
+
type CreatedAgent,
|
|
19
|
+
type DeletedAgent,
|
|
20
|
+
type OwnedAgent,
|
|
21
|
+
type ResetAgentKey,
|
|
22
|
+
} from './agent-admin.js';
|
|
23
|
+
import type { AccountRef } from './account-attach.js';
|
|
24
|
+
import {
|
|
25
|
+
AGENT_FILE,
|
|
26
|
+
agentsDir,
|
|
27
|
+
listAgentFiles,
|
|
28
|
+
readAgentFile,
|
|
29
|
+
type AgentFile,
|
|
30
|
+
} from './file-source.js';
|
|
31
|
+
import { newId } from './ids.js';
|
|
32
|
+
import { registerKey, rotateAgentKey, unregisterAgentKey } from './key-map.js';
|
|
33
|
+
import { MOVABLE_STATIONS } from './materialize.js';
|
|
34
|
+
import type { StationName } from './schema.js';
|
|
35
|
+
import { normalizeAddress } from './users.js';
|
|
36
|
+
|
|
37
|
+
export const LOCAL_PROJECT_ID = 'localdaemon';
|
|
38
|
+
const OWNER_FILE = '.owner';
|
|
39
|
+
|
|
40
|
+
interface Stored {
|
|
41
|
+
path: string;
|
|
42
|
+
file: AgentFile;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const missing = (): AgentAdminError => new AgentAdminError('no such agent', 404);
|
|
46
|
+
|
|
47
|
+
export function localOwner(dir = agentsDir()): string | null {
|
|
48
|
+
try {
|
|
49
|
+
return normalizeAddress(readFileSync(join(dir, OWNER_FILE), 'utf8'));
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeOwnerOnce(path: string, address: string): boolean {
|
|
56
|
+
let fd: number;
|
|
57
|
+
try {
|
|
58
|
+
fd = openSync(path, 'wx', 0o600);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if ((err as NodeJS.ErrnoException).code === 'EEXIST') return false;
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
writeSync(fd, `${address}\n`);
|
|
64
|
+
closeSync(fd);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function claimLocalOwner(raw: string, dir = agentsDir()): Promise<string> {
|
|
69
|
+
const address = normalizeAddress(raw);
|
|
70
|
+
if (address === null)
|
|
71
|
+
return Promise.reject(new ApiError('an Ethereum address is required', 400));
|
|
72
|
+
ensureSecureDir(dir);
|
|
73
|
+
if (writeOwnerOnce(join(dir, OWNER_FILE), address))
|
|
74
|
+
return Promise.resolve(address);
|
|
75
|
+
if (localOwner(dir) !== address)
|
|
76
|
+
return Promise.reject(
|
|
77
|
+
new ApiError('this machine belongs to another wallet', 403),
|
|
78
|
+
);
|
|
79
|
+
return Promise.resolve(address);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isOwner(subject: string, dir: string): boolean {
|
|
83
|
+
const owner = localOwner(dir);
|
|
84
|
+
return owner !== null && owner === normalizeAddress(subject);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function storedAgents(dir: string): Stored[] {
|
|
88
|
+
return listAgentFiles(dir).map((path) => ({ path, file: readAgentFile(path) }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function save(stored: Stored): void {
|
|
92
|
+
writeSecure(stored.path, `${JSON.stringify(stored.file, null, 2)}\n`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ownedOrThrow(subject: string, id: string, dir: string): Stored {
|
|
96
|
+
if (!isOwner(subject, dir)) throw missing();
|
|
97
|
+
const found = storedAgents(dir).find((s) => s.file.id === id);
|
|
98
|
+
if (found === undefined) throw missing();
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function localOwnedAgentOrThrow(
|
|
103
|
+
subject: string,
|
|
104
|
+
id: string,
|
|
105
|
+
dir = agentsDir(),
|
|
106
|
+
): Promise<{ agent: OwnedAgent }> {
|
|
107
|
+
const { file } = ownedOrThrow(subject, id, dir);
|
|
108
|
+
return Promise.resolve({ agent: { id: file.id, name: file.name } });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function localListAgents(
|
|
112
|
+
subject: string,
|
|
113
|
+
project: string,
|
|
114
|
+
dir = agentsDir(),
|
|
115
|
+
): Promise<AgentSummary[]> {
|
|
116
|
+
if (project !== LOCAL_PROJECT_ID || !isOwner(subject, dir))
|
|
117
|
+
throw new AgentAdminError('no such project', 404);
|
|
118
|
+
return Promise.resolve(
|
|
119
|
+
storedAgents(dir)
|
|
120
|
+
.map(({ file }) => ({ id: file.id, name: file.name, owned: true, key: file.key }))
|
|
121
|
+
.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id)),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function freshId(taken: Set<string>): string {
|
|
126
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
127
|
+
const id = newId();
|
|
128
|
+
if (!taken.has(id)) return id;
|
|
129
|
+
}
|
|
130
|
+
throw new AgentAdminError('could not allocate a free id', 500);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function localCreateAgent(
|
|
134
|
+
subject: string,
|
|
135
|
+
project: string,
|
|
136
|
+
rawName: string,
|
|
137
|
+
dir = agentsDir(),
|
|
138
|
+
): Promise<CreatedAgent> {
|
|
139
|
+
if (project !== LOCAL_PROJECT_ID || !isOwner(subject, dir))
|
|
140
|
+
throw new AgentAdminError('no such project', 404);
|
|
141
|
+
const name = normalizeAgentName(rawName);
|
|
142
|
+
const folder = join(dir, name);
|
|
143
|
+
if (existsSync(join(folder, AGENT_FILE)))
|
|
144
|
+
throw new AgentAdminError(
|
|
145
|
+
`an agent named '${name}' already exists on this machine`,
|
|
146
|
+
409,
|
|
147
|
+
);
|
|
148
|
+
const existing = storedAgents(dir);
|
|
149
|
+
const id = freshId(new Set(existing.map((s) => s.file.id)));
|
|
150
|
+
const key = newApiKey();
|
|
151
|
+
ensureSecureDir(folder);
|
|
152
|
+
save({
|
|
153
|
+
path: join(folder, AGENT_FILE),
|
|
154
|
+
file: { version: 1, id, name, key, owner: localOwner(dir), stations: [] },
|
|
155
|
+
});
|
|
156
|
+
registerKey(key, id);
|
|
157
|
+
return Promise.resolve({ id, name, key });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function localResetAgentKey(
|
|
161
|
+
subject: string,
|
|
162
|
+
id: string,
|
|
163
|
+
dir = agentsDir(),
|
|
164
|
+
): Promise<ResetAgentKey> {
|
|
165
|
+
const stored = ownedOrThrow(subject, id, dir);
|
|
166
|
+
const key = newApiKey();
|
|
167
|
+
stored.file.key = key;
|
|
168
|
+
save(stored);
|
|
169
|
+
rotateAgentKey(id, key);
|
|
170
|
+
return Promise.resolve({ id, name: stored.file.name, key });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function localDeleteAgent(
|
|
174
|
+
subject: string,
|
|
175
|
+
id: string,
|
|
176
|
+
dir = agentsDir(),
|
|
177
|
+
): Promise<DeletedAgent> {
|
|
178
|
+
const stored = ownedOrThrow(subject, id, dir);
|
|
179
|
+
const attached = stored.file.stations.length;
|
|
180
|
+
if (attached > 0)
|
|
181
|
+
throw new AgentAdminError(
|
|
182
|
+
`agent '${stored.file.name}' still has ${String(attached)} station account(s) attached — detach them first`,
|
|
183
|
+
409,
|
|
184
|
+
);
|
|
185
|
+
rmSync(stored.path);
|
|
186
|
+
removeIfEmpty(join(stored.path, '..'));
|
|
187
|
+
unregisterAgentKey(id);
|
|
188
|
+
return Promise.resolve({ id, name: stored.file.name });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function removeIfEmpty(folder: string): void {
|
|
192
|
+
try {
|
|
193
|
+
rmdirSync(folder);
|
|
194
|
+
} catch {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function assertTokenFree(all: Stored[], station: StationName, token: string): void {
|
|
200
|
+
const taken = all.some((s) =>
|
|
201
|
+
s.file.stations.some(
|
|
202
|
+
(a) => a.station === station && a.config.token === token,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
if (taken)
|
|
206
|
+
throw new AgentAdminError(
|
|
207
|
+
'that bot token is already attached to an agent on this machine',
|
|
208
|
+
409,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function localAttachAccount(
|
|
213
|
+
subject: string,
|
|
214
|
+
agentId: string,
|
|
215
|
+
station: StationName,
|
|
216
|
+
config: Record<string, unknown>,
|
|
217
|
+
dir = agentsDir(),
|
|
218
|
+
): Promise<AccountRef> {
|
|
219
|
+
const stored = ownedOrThrow(subject, agentId, dir);
|
|
220
|
+
if (!MOVABLE_STATIONS.has(station))
|
|
221
|
+
throw new AgentAdminError(
|
|
222
|
+
`a ${station} endpoint needs a public url and cannot live on a local daemon`,
|
|
223
|
+
400,
|
|
224
|
+
);
|
|
225
|
+
const token = config.token;
|
|
226
|
+
if (typeof token === 'string') assertTokenFree(storedAgents(dir), station, token);
|
|
227
|
+
const taken = new Set(stored.file.stations.map((a) => a.id));
|
|
228
|
+
const accountId = freshId(taken);
|
|
229
|
+
stored.file.stations.push({ station, id: accountId, allowlist: ['*'], config });
|
|
230
|
+
save(stored);
|
|
231
|
+
return Promise.resolve({ agentId, station, accountId });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function localDetachAccount(
|
|
235
|
+
subject: string,
|
|
236
|
+
agentId: string,
|
|
237
|
+
station: StationName,
|
|
238
|
+
accountId: string,
|
|
239
|
+
dir = agentsDir(),
|
|
240
|
+
): Promise<AccountRef> {
|
|
241
|
+
const stored = ownedOrThrow(subject, agentId, dir);
|
|
242
|
+
const before = stored.file.stations.length;
|
|
243
|
+
stored.file.stations = stored.file.stations.filter(
|
|
244
|
+
(a) => !(a.station === station && a.id === accountId),
|
|
245
|
+
);
|
|
246
|
+
if (stored.file.stations.length === before)
|
|
247
|
+
throw new AgentAdminError('no such account on this agent', 404);
|
|
248
|
+
save(stored);
|
|
249
|
+
return Promise.resolve({ agentId, station, accountId });
|
|
250
|
+
}
|