@stage-labs/metro 0.1.0-beta.50 → 0.1.0-beta.52
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 +8 -142
- package/dist/cli.js +27 -131
- package/dist/control.js +5 -104
- package/dist/plugin.js +2 -4
- package/dist/runtime.js +18 -72
- package/dist/serve.js +3 -2
- package/dist/tail.js +7 -8
- package/dist/update.js +13 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/package.json +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +3 -35
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +7 -22
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +4 -54
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +1 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +29 -208
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +0 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +2 -37
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +0 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +4 -6
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +3 -14
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +0 -81
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +119 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +42 -12
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +1 -73
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +1 -172
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +0 -5
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +2 -89
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +3 -262
- package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +2 -140
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +1 -89
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +14 -41
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +5 -5
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +0 -2
- package/runtime/runtime.json +1 -1
- package/dist/prompt.js +0 -61
- package/dist/store.js +0 -93
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +0 -76
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +0 -20
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +0 -141
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +0 -68
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +0 -58
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +0 -214
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +0 -139
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +0 -105
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +0 -143
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +0 -309
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +0 -24
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +0 -105
- package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +0 -18
|
@@ -6,13 +6,10 @@ import {
|
|
|
6
6
|
writeFileSync,
|
|
7
7
|
} from 'node:fs';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
|
-
import { ID_RE } from './ids.js';
|
|
10
9
|
import { join } from 'node:path';
|
|
11
|
-
import { eq } from 'drizzle-orm';
|
|
12
10
|
import { log } from '../daemon/log.js';
|
|
13
11
|
import { writeSecure } from '../daemon/secure-fs.js';
|
|
14
12
|
import { isLocalMode, trainsDir } from '../daemon/paths.js';
|
|
15
|
-
import { closeDb, databaseUrl, getDb } from './client.js';
|
|
16
13
|
import {
|
|
17
14
|
setAgentMap,
|
|
18
15
|
setAllowlistMap,
|
|
@@ -21,14 +18,7 @@ import {
|
|
|
21
18
|
type AllowlistMap,
|
|
22
19
|
} from './agent-map.js';
|
|
23
20
|
import { setKeyMap } from './key-map.js';
|
|
24
|
-
import {
|
|
25
|
-
agentConnectors,
|
|
26
|
-
agents,
|
|
27
|
-
connectors,
|
|
28
|
-
stations,
|
|
29
|
-
STATIONS,
|
|
30
|
-
type StationName,
|
|
31
|
-
} from './schema.js';
|
|
21
|
+
import { STATIONS, type StationName } from './schema.js';
|
|
32
22
|
|
|
33
23
|
interface StationTarget {
|
|
34
24
|
file: string;
|
|
@@ -99,116 +89,12 @@ function accountFilePath(station: StationName): string {
|
|
|
99
89
|
return process.env[target.fileEnv] ?? join(METRO_DIR, target.file);
|
|
100
90
|
}
|
|
101
91
|
|
|
102
|
-
function agentFilter(): string | undefined {
|
|
103
|
-
const v = process.env.METRO_AGENT?.trim();
|
|
104
|
-
if (!v) return undefined;
|
|
105
|
-
if (!ID_RE.test(v))
|
|
106
|
-
throw new Error(`METRO_AGENT must be an 11-character agent id, got '${v}'`);
|
|
107
|
-
return v;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
92
|
export type StationSource = () => Promise<LoadedAgent[]>;
|
|
111
93
|
|
|
112
94
|
export const MOVABLE_STATIONS = new Set<StationName>(
|
|
113
95
|
STATIONS.filter((s) => s !== 'webhook'),
|
|
114
96
|
);
|
|
115
97
|
|
|
116
|
-
export function unmovableStations(list: LoadedAccount[]): StationName[] {
|
|
117
|
-
return [
|
|
118
|
-
...new Set(
|
|
119
|
-
list
|
|
120
|
-
.map((a) => a.station)
|
|
121
|
-
.filter((station) => !MOVABLE_STATIONS.has(station)),
|
|
122
|
-
),
|
|
123
|
-
];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export async function loadAllStationsFor(
|
|
127
|
-
agentId: string,
|
|
128
|
-
): Promise<LoadedAccount[]> {
|
|
129
|
-
const rows = await getDb()
|
|
130
|
-
.select()
|
|
131
|
-
.from(stations)
|
|
132
|
-
.where(eq(stations.agentId, agentId));
|
|
133
|
-
return rows.map((r) => ({
|
|
134
|
-
station: r.station,
|
|
135
|
-
id: r.id,
|
|
136
|
-
allowlist: r.allowlist,
|
|
137
|
-
config: r.config as Record<string, unknown>,
|
|
138
|
-
}));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function loadAgentForRuntime(
|
|
142
|
-
agentId: string,
|
|
143
|
-
): Promise<LoadedAgent> {
|
|
144
|
-
const db = getDb();
|
|
145
|
-
const rows = await db.select().from(agents).where(eq(agents.id, agentId));
|
|
146
|
-
const a = rows[0];
|
|
147
|
-
if (a === undefined) throw new Error(`no such agent '${agentId}'`);
|
|
148
|
-
const acctRows = await db
|
|
149
|
-
.select()
|
|
150
|
-
.from(stations)
|
|
151
|
-
.where(eq(stations.agentId, a.id));
|
|
152
|
-
const held = await db
|
|
153
|
-
.select({ id: connectors.id, name: connectors.name, url: connectors.url, config: connectors.config })
|
|
154
|
-
.from(agentConnectors)
|
|
155
|
-
.innerJoin(connectors, eq(connectors.id, agentConnectors.connectorId))
|
|
156
|
-
.where(eq(agentConnectors.agentId, a.id));
|
|
157
|
-
return {
|
|
158
|
-
id: a.id,
|
|
159
|
-
name: a.name,
|
|
160
|
-
key: a.key,
|
|
161
|
-
connectors: held.map((c) => ({
|
|
162
|
-
id: c.id,
|
|
163
|
-
name: c.name,
|
|
164
|
-
url: c.url,
|
|
165
|
-
transport: 'http' as const,
|
|
166
|
-
config: c.config as Record<string, unknown>,
|
|
167
|
-
})),
|
|
168
|
-
accounts: acctRows
|
|
169
|
-
.filter((r) => MOVABLE_STATIONS.has(r.station))
|
|
170
|
-
.map((r) => ({
|
|
171
|
-
station: r.station,
|
|
172
|
-
id: r.id,
|
|
173
|
-
allowlist: r.allowlist,
|
|
174
|
-
config: r.config as Record<string, unknown>,
|
|
175
|
-
})),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const pgSource: StationSource = () => loadAgents();
|
|
180
|
-
|
|
181
|
-
async function loadAgents(): Promise<LoadedAgent[]> {
|
|
182
|
-
const db = getDb();
|
|
183
|
-
const only = agentFilter();
|
|
184
|
-
const agentRows =
|
|
185
|
-
only !== undefined
|
|
186
|
-
? await db.select().from(agents).where(eq(agents.id, only))
|
|
187
|
-
: await db.select().from(agents);
|
|
188
|
-
if (only !== undefined && agentRows.length === 0)
|
|
189
|
-
throw new Error(`METRO_AGENT=${only} does not match any agent`);
|
|
190
|
-
|
|
191
|
-
const out: LoadedAgent[] = [];
|
|
192
|
-
for (const a of agentRows) {
|
|
193
|
-
const acctRows = await db
|
|
194
|
-
.select()
|
|
195
|
-
.from(stations)
|
|
196
|
-
.where(eq(stations.agentId, a.id));
|
|
197
|
-
out.push({
|
|
198
|
-
id: a.id,
|
|
199
|
-
name: a.name,
|
|
200
|
-
accounts: acctRows.map((r) => ({
|
|
201
|
-
station: r.station,
|
|
202
|
-
id: r.id,
|
|
203
|
-
allowlist: r.allowlist,
|
|
204
|
-
config: r.config as Record<string, unknown>,
|
|
205
|
-
})),
|
|
206
|
-
key: a.runtimeId === null ? a.key : null,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
return out;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
98
|
function trainStubPath(station: StationName): string {
|
|
213
99
|
return join(trainsDir(), `${station}.ts`);
|
|
214
100
|
}
|
|
@@ -227,11 +113,8 @@ export function writeIfChanged(path: string, content: string): boolean {
|
|
|
227
113
|
return true;
|
|
228
114
|
}
|
|
229
115
|
|
|
230
|
-
const isLocalRuntime = (): boolean =>
|
|
231
|
-
(process.env.METRO_RUN_TOKEN?.trim() ?? '') !== '';
|
|
232
|
-
|
|
233
116
|
export const stationRunsHere = (station: StationName): boolean =>
|
|
234
|
-
|
|
117
|
+
isLocalMode() || !MOVABLE_STATIONS.has(station);
|
|
235
118
|
|
|
236
119
|
interface WrittenStations {
|
|
237
120
|
active: Map<StationName, number>;
|
|
@@ -350,16 +233,6 @@ export async function materializeFrom(
|
|
|
350
233
|
);
|
|
351
234
|
}
|
|
352
235
|
|
|
353
|
-
export async function materializeFromDb(): Promise<void> {
|
|
354
|
-
if (!databaseUrl())
|
|
355
|
-
throw new Error('DATABASE_URL is not set — accounts load from Postgres');
|
|
356
|
-
try {
|
|
357
|
-
await materializeFrom(pgSource);
|
|
358
|
-
} finally {
|
|
359
|
-
await closeDb();
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
236
|
export interface ReloadedStations {
|
|
364
237
|
active: StationName[];
|
|
365
238
|
removed: StationName[];
|
|
@@ -378,14 +251,3 @@ export async function reloadFrom(
|
|
|
378
251
|
);
|
|
379
252
|
return { active: [...active.keys()], removed, changed };
|
|
380
253
|
}
|
|
381
|
-
|
|
382
|
-
export async function reloadAccountsFromDb(): Promise<ReloadedStations> {
|
|
383
|
-
if (!databaseUrl())
|
|
384
|
-
throw new Error('DATABASE_URL is not set — accounts load from Postgres');
|
|
385
|
-
return reloadFrom(pgSource);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (import.meta.main) {
|
|
389
|
-
await materializeFromDb();
|
|
390
|
-
process.exit(0);
|
|
391
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { index, jsonb, pgTable, text } from 'drizzle-orm/pg-core';
|
|
2
2
|
|
|
3
3
|
export const STATIONS = [
|
|
4
4
|
'xmtp',
|
|
@@ -17,98 +17,10 @@ export const users = pgTable('users', {
|
|
|
17
17
|
address: text('address').unique(),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
export type ProjectRole = 'admin' | 'member';
|
|
22
21
|
|
|
23
|
-
export const projects = pgTable('projects', {
|
|
24
|
-
id: text('id').primaryKey(),
|
|
25
|
-
name: text('name').notNull(),
|
|
26
|
-
ownerId: text('owner_id')
|
|
27
|
-
.notNull()
|
|
28
|
-
.references(() => users.id, { onDelete: 'restrict' }),
|
|
29
|
-
isDefault: boolean('is_default').notNull().default(false),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
export const projectMembers = pgTable(
|
|
33
|
-
'project_members',
|
|
34
|
-
{
|
|
35
|
-
id: text('id').primaryKey(),
|
|
36
|
-
projectId: text('project_id')
|
|
37
|
-
.notNull()
|
|
38
|
-
.references(() => projects.id, { onDelete: 'cascade' }),
|
|
39
|
-
userId: text('user_id')
|
|
40
|
-
.notNull()
|
|
41
|
-
.references(() => users.id, { onDelete: 'restrict' }),
|
|
42
|
-
role: text('role').$type<ProjectRole>().notNull(),
|
|
43
|
-
},
|
|
44
|
-
(t) => [unique('project_members_project_id_user_id_unique').on(t.projectId, t.userId)],
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
export const agents = pgTable('agents', {
|
|
48
|
-
id: text('id').primaryKey(),
|
|
49
|
-
name: text('name').notNull(),
|
|
50
|
-
projectId: text('project_id')
|
|
51
|
-
.notNull()
|
|
52
|
-
.references(() => projects.id, { onDelete: 'restrict' }),
|
|
53
|
-
key: text('key').unique(),
|
|
54
|
-
runtimeId: text('runtime_id'),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
export const runtimes = pgTable('runtimes', {
|
|
58
|
-
id: text('id').primaryKey(),
|
|
59
|
-
agentId: text('agent_id')
|
|
60
|
-
.notNull()
|
|
61
|
-
.references(() => agents.id, { onDelete: 'cascade' }),
|
|
62
|
-
label: text('label').notNull(),
|
|
63
|
-
createdAt: text('created_at').notNull(),
|
|
64
|
-
lastSeenAt: text('last_seen_at'),
|
|
65
|
-
revokedAt: text('revoked_at'),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
export const stations = pgTable('stations', {
|
|
69
|
-
id: text('id').primaryKey(),
|
|
70
|
-
agentId: text('agent_id').notNull(),
|
|
71
|
-
station: text('station').$type<StationName>().notNull(),
|
|
72
|
-
allowlist: text('allowlist').array().default(['*']),
|
|
73
|
-
config: jsonb('config').notNull(),
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
|
|
77
22
|
export type ConnectorTransport = 'http' | 'sse';
|
|
78
23
|
|
|
79
|
-
export const connectors = pgTable(
|
|
80
|
-
'connectors',
|
|
81
|
-
{
|
|
82
|
-
id: text('id').primaryKey(),
|
|
83
|
-
projectId: text('project_id')
|
|
84
|
-
.notNull()
|
|
85
|
-
.references(() => projects.id, { onDelete: 'restrict' }),
|
|
86
|
-
name: text('name').notNull(),
|
|
87
|
-
url: text('url').notNull(),
|
|
88
|
-
transport: text('transport').$type<ConnectorTransport>().notNull(),
|
|
89
|
-
config: jsonb('config').notNull(),
|
|
90
|
-
},
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
export const agentConnectors = pgTable(
|
|
94
|
-
'agent_connectors',
|
|
95
|
-
{
|
|
96
|
-
id: text('id').primaryKey(),
|
|
97
|
-
agentId: text('agent_id')
|
|
98
|
-
.notNull()
|
|
99
|
-
.references(() => agents.id, { onDelete: 'cascade' }),
|
|
100
|
-
connectorId: text('connector_id')
|
|
101
|
-
.notNull()
|
|
102
|
-
.references(() => connectors.id, { onDelete: 'cascade' }),
|
|
103
|
-
},
|
|
104
|
-
(t) => [
|
|
105
|
-
unique('agent_connectors_agent_id_connector_id_unique').on(
|
|
106
|
-
t.agentId,
|
|
107
|
-
t.connectorId,
|
|
108
|
-
),
|
|
109
|
-
],
|
|
110
|
-
);
|
|
111
|
-
|
|
112
24
|
export const vault = pgTable(
|
|
113
25
|
'vault',
|
|
114
26
|
{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
2
|
import { ApiError } from '../daemon/api-error.js';
|
|
3
3
|
import { getDb } from './client.js';
|
|
4
4
|
import { newId } from './ids.js';
|
|
@@ -22,51 +22,24 @@ export function isUniqueViolation(err: unknown): boolean {
|
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function normalizeEmail(raw: string): string {
|
|
26
|
-
return raw.trim().toLowerCase();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
export function normalizeAddress(raw: string): string | null {
|
|
30
26
|
const address = raw.trim().toLowerCase();
|
|
31
27
|
return ADDRESS_RE.test(address) ? address : null;
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
async function userIdWhere(where: SQL): Promise<string | null> {
|
|
35
|
-
const rows = await getDb().select({ id: users.id }).from(users).where(where);
|
|
36
|
-
return rows[0]?.id ?? null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function userIdForSubject(subject: string): Promise<string | null> {
|
|
40
|
-
const address = normalizeAddress(subject);
|
|
41
|
-
if (address !== null) return userIdWhere(eq(users.address, address));
|
|
42
|
-
return userIdWhere(eq(users.email, normalizeEmail(subject)));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function resolveUserId(
|
|
46
|
-
insert: () => Promise<string | undefined>,
|
|
47
|
-
lookup: () => Promise<string | null>,
|
|
48
|
-
): Promise<string> {
|
|
49
|
-
const inserted = await insert();
|
|
50
|
-
if (inserted !== undefined) return inserted;
|
|
51
|
-
const existing = await lookup();
|
|
52
|
-
if (existing === null)
|
|
53
|
-
throw new UserError('user lookup returned no id', 500);
|
|
54
|
-
return existing;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
30
|
export async function ensureUserByAddress(raw: string): Promise<string> {
|
|
58
31
|
const address = normalizeAddress(raw);
|
|
59
|
-
if (address === null)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
32
|
+
if (address === null) throw new UserError('an Ethereum address is required', 400);
|
|
33
|
+
const db = getDb();
|
|
34
|
+
const inserted = await db
|
|
35
|
+
.insert(users)
|
|
36
|
+
.values({ id: newId(), address })
|
|
37
|
+
.onConflictDoNothing({ target: users.address })
|
|
38
|
+
.returning({ id: users.id });
|
|
39
|
+
const id = inserted[0]?.id;
|
|
40
|
+
if (id !== undefined) return id;
|
|
41
|
+
const rows = await db.select({ id: users.id }).from(users).where(eq(users.address, address));
|
|
42
|
+
const existing = rows[0]?.id;
|
|
43
|
+
if (existing === undefined) throw new UserError('user lookup returned no id', 500);
|
|
44
|
+
return existing;
|
|
72
45
|
}
|
|
@@ -32,7 +32,7 @@ const missing = (): VaultError => new VaultError('no such bundle', 404);
|
|
|
32
32
|
|
|
33
33
|
function ownerOf(subject: string): string {
|
|
34
34
|
const address = normalizeAddress(subject);
|
|
35
|
-
if (address === null) throw new VaultError('the vault needs a
|
|
35
|
+
if (address === null) throw new VaultError('the vault needs a vault identity', 403);
|
|
36
36
|
return address;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -44,12 +44,12 @@ function stationsOf(raw: unknown): string[] {
|
|
|
44
44
|
return [...new Set(list)];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function envelopeOf(raw: unknown, id: string
|
|
47
|
+
function envelopeOf(raw: unknown, id: string): Record<string, unknown> {
|
|
48
48
|
if (!isRecord(raw) || raw.v !== 1 || raw.agentId !== id)
|
|
49
49
|
throw new VaultError('envelope must be a v1 envelope for this agent', 400);
|
|
50
50
|
const key = raw.key;
|
|
51
|
-
if (!isRecord(key) || key.recipient
|
|
52
|
-
throw new VaultError('envelope must
|
|
51
|
+
if (!isRecord(key) || normalizeAddress(typeof key.recipient === 'string' ? key.recipient : '') === null)
|
|
52
|
+
throw new VaultError('envelope must name the wallet it was sealed to', 400);
|
|
53
53
|
if (typeof raw.ciphertext !== 'string' || typeof raw.nonce !== 'string')
|
|
54
54
|
throw new VaultError('envelope carries no ciphertext', 400);
|
|
55
55
|
if (JSON.stringify(raw).length > ENVELOPE_MAX)
|
|
@@ -63,7 +63,7 @@ export function parseVaultInput(subject: string, id: string, body: unknown): Vau
|
|
|
63
63
|
if (!isRecord(body)) throw new VaultError('body must be an object', 400);
|
|
64
64
|
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
65
65
|
if (!AGENT_NAME_RE.test(name)) throw new VaultError('name is not an agent name', 400);
|
|
66
|
-
return { owner, name, stations: stationsOf(body.stations), envelope: envelopeOf(body.envelope, id
|
|
66
|
+
return { owner, name, stations: stationsOf(body.stations), envelope: envelopeOf(body.envelope, id) };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const entryOf = (row: { id: string; name: string; stations: unknown; syncedAt: string }): VaultEntry => ({
|
package/runtime/runtime.json
CHANGED
package/dist/prompt.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline';
|
|
2
|
-
const CTRL_C = '\u0003';
|
|
3
|
-
const BACKSPACE = '\u007f';
|
|
4
|
-
function askLine(question) {
|
|
5
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
rl.question(question, (answer) => {
|
|
8
|
-
rl.close();
|
|
9
|
-
resolve(answer.trim());
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
export function feedSecret(value, chunk) {
|
|
14
|
-
let out = value;
|
|
15
|
-
for (const ch of chunk) {
|
|
16
|
-
if (ch === CTRL_C)
|
|
17
|
-
return { kind: 'cancelled' };
|
|
18
|
-
if (ch === '\r' || ch === '\n')
|
|
19
|
-
return { kind: 'done', value: out };
|
|
20
|
-
if (ch === BACKSPACE || ch === '\b') {
|
|
21
|
-
out = out.slice(0, -1);
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (ch >= ' ')
|
|
25
|
-
out += ch;
|
|
26
|
-
}
|
|
27
|
-
return { kind: 'more', value: out };
|
|
28
|
-
}
|
|
29
|
-
export function askSecret(question) {
|
|
30
|
-
const stdin = process.stdin;
|
|
31
|
-
if (!stdin.isTTY)
|
|
32
|
-
return askLine(question);
|
|
33
|
-
process.stderr.write(question);
|
|
34
|
-
stdin.setRawMode(true);
|
|
35
|
-
stdin.resume();
|
|
36
|
-
stdin.setEncoding('utf8');
|
|
37
|
-
return new Promise((resolve, reject) => {
|
|
38
|
-
let value = '';
|
|
39
|
-
const finish = () => {
|
|
40
|
-
stdin.setRawMode(false);
|
|
41
|
-
stdin.pause();
|
|
42
|
-
stdin.off('data', onData);
|
|
43
|
-
process.stderr.write('\n');
|
|
44
|
-
};
|
|
45
|
-
const onData = (chunk) => {
|
|
46
|
-
const step = feedSecret(value, chunk);
|
|
47
|
-
if (step.kind === 'cancelled') {
|
|
48
|
-
finish();
|
|
49
|
-
reject(new Error('cancelled'));
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (step.kind === 'done') {
|
|
53
|
-
finish();
|
|
54
|
-
resolve(step.value.trim());
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
value = step.value;
|
|
58
|
-
};
|
|
59
|
-
stdin.on('data', onData);
|
|
60
|
-
});
|
|
61
|
-
}
|
package/dist/store.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { chmodSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
const DEFAULT_BASE = 'https://mcp.metro.box';
|
|
5
|
-
const DEFAULT_WEB = 'https://metro.box';
|
|
6
|
-
function fromEnv(name, fallback) {
|
|
7
|
-
const raw = process.env[name]?.trim();
|
|
8
|
-
return raw === undefined || raw === '' ? fallback : raw.replace(/\/+$/, '');
|
|
9
|
-
}
|
|
10
|
-
export function metroUrl() {
|
|
11
|
-
return fromEnv('METRO_URL', DEFAULT_BASE);
|
|
12
|
-
}
|
|
13
|
-
export function metroWebUrl() {
|
|
14
|
-
return fromEnv('METRO_UI_URL', DEFAULT_WEB);
|
|
15
|
-
}
|
|
16
|
-
function configDir() {
|
|
17
|
-
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
18
|
-
const base = xdg === undefined || xdg === '' ? join(homedir(), '.config') : xdg;
|
|
19
|
-
return join(base, 'metro');
|
|
20
|
-
}
|
|
21
|
-
export function credentialsPath() {
|
|
22
|
-
return join(configDir(), 'credentials.json');
|
|
23
|
-
}
|
|
24
|
-
function readStoredAt(path) {
|
|
25
|
-
let raw;
|
|
26
|
-
try {
|
|
27
|
-
raw = readFileSync(path, 'utf8');
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
let parsed;
|
|
33
|
-
try {
|
|
34
|
-
parsed = JSON.parse(raw);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
if (typeof parsed !== 'object' || parsed === null)
|
|
40
|
-
return null;
|
|
41
|
-
const { token, url } = parsed;
|
|
42
|
-
if (typeof token !== 'string' || token === '')
|
|
43
|
-
return null;
|
|
44
|
-
return { token, url: typeof url === 'string' ? url : DEFAULT_BASE };
|
|
45
|
-
}
|
|
46
|
-
const readStored = () => readStoredAt(credentialsPath());
|
|
47
|
-
const RUN_TOKEN_FILE = /^runtime-[A-Za-z0-9_-]{11}\.json$/;
|
|
48
|
-
export function readRunTokens() {
|
|
49
|
-
const fromEnv = process.env.METRO_RUN_TOKEN?.trim();
|
|
50
|
-
if (fromEnv !== undefined && fromEnv !== '')
|
|
51
|
-
return [fromEnv];
|
|
52
|
-
let names;
|
|
53
|
-
try {
|
|
54
|
-
names = readdirSync(configDir());
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
const out = [];
|
|
60
|
-
for (const name of names) {
|
|
61
|
-
if (!RUN_TOKEN_FILE.test(name))
|
|
62
|
-
continue;
|
|
63
|
-
const stored = readStoredAt(join(configDir(), name));
|
|
64
|
-
if (stored !== null && stored.url === metroUrl())
|
|
65
|
-
out.push(stored.token);
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
export function readToken() {
|
|
70
|
-
const fromEnv = process.env.METRO_TOKEN?.trim();
|
|
71
|
-
if (fromEnv !== undefined && fromEnv !== '')
|
|
72
|
-
return fromEnv;
|
|
73
|
-
const stored = readStored();
|
|
74
|
-
if (stored === null)
|
|
75
|
-
return null;
|
|
76
|
-
return stored.url === metroUrl() ? stored.token : null;
|
|
77
|
-
}
|
|
78
|
-
export function writeToken(token) {
|
|
79
|
-
const path = credentialsPath();
|
|
80
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
81
|
-
writeFileSync(path, `${JSON.stringify({ token, url: metroUrl() })}\n`, {
|
|
82
|
-
mode: 0o600,
|
|
83
|
-
});
|
|
84
|
-
chmodSync(path, 0o600);
|
|
85
|
-
}
|
|
86
|
-
export function clearToken() {
|
|
87
|
-
try {
|
|
88
|
-
rmSync(credentialsPath());
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { ApiError } from './api-error.js';
|
|
4
|
-
import { configDir } from './paths.js';
|
|
5
|
-
import { writeSecure } from './secure-fs.js';
|
|
6
|
-
import { carriesSecretsSafely, httpSource } from './runtime-source.js';
|
|
7
|
-
import type { LoadedAgent } from '../db/materialize.js';
|
|
8
|
-
|
|
9
|
-
const CLAIM_TIMEOUT_MS = 15_000;
|
|
10
|
-
const CODE_RE = /^ma_[A-Za-z0-9_-]{16}$/;
|
|
11
|
-
|
|
12
|
-
function metroBaseUrl(): string {
|
|
13
|
-
const raw = (process.env.METRO_URL?.trim() ?? '').replace(/\/+$/, '');
|
|
14
|
-
const url = raw === '' ? 'https://mcp.metro.box' : raw;
|
|
15
|
-
if (!carriesSecretsSafely(url))
|
|
16
|
-
throw new ApiError(`refusing to read station credentials from ${url} in the clear`, 400);
|
|
17
|
-
return url;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function parsePairingCode(raw: unknown): string {
|
|
21
|
-
const code = typeof raw === 'string' ? raw.trim() : '';
|
|
22
|
-
if (!CODE_RE.test(code))
|
|
23
|
-
throw new ApiError('paste the pairing code from the agent\'s page on metro.box', 400);
|
|
24
|
-
return code;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function claim(base: string, code: string, label: string): Promise<string> {
|
|
28
|
-
let res: Response;
|
|
29
|
-
try {
|
|
30
|
-
res = await fetch(`${base}/api/run/claim`, {
|
|
31
|
-
method: 'POST',
|
|
32
|
-
headers: { 'content-type': 'application/json' },
|
|
33
|
-
body: JSON.stringify({ code, label }),
|
|
34
|
-
signal: AbortSignal.timeout(CLAIM_TIMEOUT_MS),
|
|
35
|
-
redirect: 'manual',
|
|
36
|
-
});
|
|
37
|
-
} catch {
|
|
38
|
-
throw new ApiError(`could not reach ${base}`, 502);
|
|
39
|
-
}
|
|
40
|
-
const body: unknown = await res.json().catch(() => null);
|
|
41
|
-
const field = (key: string): unknown =>
|
|
42
|
-
typeof body === 'object' && body !== null ? (body as Record<string, unknown>)[key] : undefined;
|
|
43
|
-
if (!res.ok) {
|
|
44
|
-
const reason = field('error');
|
|
45
|
-
throw new ApiError(
|
|
46
|
-
typeof reason === 'string' ? reason : `metro answered ${String(res.status)} for the code`,
|
|
47
|
-
res.status === 404 ? 400 : res.status,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
const token = field('token');
|
|
51
|
-
if (typeof token !== 'string' || token === '')
|
|
52
|
-
throw new ApiError('metro returned no runtime token', 502);
|
|
53
|
-
return token;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function storeRunToken(agentId: string, token: string, url: string): string {
|
|
57
|
-
const dir = configDir();
|
|
58
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
59
|
-
const path = join(dir, `runtime-${agentId}.json`);
|
|
60
|
-
writeSecure(path, `${JSON.stringify({ token, url }, null, 2)}\n`);
|
|
61
|
-
return path;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function fetchAgentWithCode(
|
|
65
|
-
code: string,
|
|
66
|
-
label: string,
|
|
67
|
-
base = metroBaseUrl(),
|
|
68
|
-
): Promise<LoadedAgent> {
|
|
69
|
-
const token = await claim(base, code, label);
|
|
70
|
-
const agents = await httpSource({ url: base, token })();
|
|
71
|
-
const agent = agents[0];
|
|
72
|
-
if (agent === undefined || agents.length !== 1)
|
|
73
|
-
throw new ApiError('metro returned no agent for that code', 502);
|
|
74
|
-
storeRunToken(agent.id, token, base);
|
|
75
|
-
return agent;
|
|
76
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { makeCodeStore } from './pair-codes.js';
|
|
2
|
-
|
|
3
|
-
export interface AgentCode {
|
|
4
|
-
subject: string;
|
|
5
|
-
agentId: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const store = makeCodeStore<AgentCode>('ma');
|
|
9
|
-
|
|
10
|
-
export const AGENT_CODE_RE = store.pattern;
|
|
11
|
-
|
|
12
|
-
export const mintAgentCode = (
|
|
13
|
-
entry: AgentCode,
|
|
14
|
-
now?: number,
|
|
15
|
-
): { code: string; expiresAt: number } => store.mint(entry, now);
|
|
16
|
-
|
|
17
|
-
export const takeAgentCode = (code: string, now?: number): AgentCode | undefined =>
|
|
18
|
-
store.take(code, now);
|
|
19
|
-
|
|
20
|
-
export const agentCodeCount = (): number => store.count();
|