@stage-labs/metro 0.1.0-beta.97 → 0.1.0-beta.98
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/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +33 -2
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +17 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +7 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +8 -0
- package/runtime/runtime.json +1 -1
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.98",
|
|
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": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.98",
|
|
4
4
|
"description": "Metro connectors for Claude Code. Runs beside the metro daemon on this machine: each connector the agent holds is one MCP server relayed through the daemon, the daemon keeps that list current on its own, and /reload-plugins picks up a new one without leaving your session. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -74,6 +74,13 @@ export interface AccountApiDeps {
|
|
|
74
74
|
allowlist: string[],
|
|
75
75
|
) => Promise<string[]>;
|
|
76
76
|
recentSenders: (station: StationName, accountId: string) => RecentSender[];
|
|
77
|
+
setAccountEnabled: (
|
|
78
|
+
subject: string,
|
|
79
|
+
agentId: string,
|
|
80
|
+
station: StationName,
|
|
81
|
+
accountId: string,
|
|
82
|
+
enabled: boolean,
|
|
83
|
+
) => Promise<boolean>;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
export type AccountRoute =
|
|
@@ -82,7 +89,8 @@ export type AccountRoute =
|
|
|
82
89
|
| { kind: 'step'; attachId: string }
|
|
83
90
|
| { kind: 'account'; station: StationName; accountId: string }
|
|
84
91
|
| { kind: 'allowlist'; station: StationName; accountId: string }
|
|
85
|
-
| { kind: 'senders'; station: StationName; accountId: string }
|
|
92
|
+
| { kind: 'senders'; station: StationName; accountId: string }
|
|
93
|
+
| { kind: 'enabled'; station: StationName; accountId: string };
|
|
86
94
|
|
|
87
95
|
export const ATTACHABLE: string[] = [
|
|
88
96
|
...ATTACHABLE_STATIONS,
|
|
@@ -96,6 +104,7 @@ const ROUTE_METHODS: Record<AccountRoute['kind'], string[]> = {
|
|
|
96
104
|
account: ['DELETE'],
|
|
97
105
|
allowlist: ['PUT'],
|
|
98
106
|
senders: ['GET'],
|
|
107
|
+
enabled: ['PUT'],
|
|
99
108
|
};
|
|
100
109
|
|
|
101
110
|
function twoSegmentRoute(head: string, tail: string): AccountRoute | null {
|
|
@@ -121,7 +130,7 @@ export function accountRoute(rest: string[]): AccountRoute | null {
|
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
function accountSubRoute(head: string, tail: string, sub: string | undefined): AccountRoute | null {
|
|
124
|
-
if (!isStationName(head) || (sub !== 'allowlist' && sub !== 'senders')) return null;
|
|
133
|
+
if (!isStationName(head) || (sub !== 'allowlist' && sub !== 'senders' && sub !== 'enabled')) return null;
|
|
125
134
|
const accountId = parseAccountId(tail);
|
|
126
135
|
return accountId === null ? null : { kind: sub, station: head, accountId };
|
|
127
136
|
}
|
|
@@ -294,6 +303,27 @@ async function handleAllowlist(
|
|
|
294
303
|
});
|
|
295
304
|
}
|
|
296
305
|
|
|
306
|
+
async function handleEnabled(
|
|
307
|
+
req: IncomingMessage,
|
|
308
|
+
res: ServerResponse,
|
|
309
|
+
deps: AccountApiDeps,
|
|
310
|
+
session: ApiSession,
|
|
311
|
+
agentId: string,
|
|
312
|
+
target: { station: StationName; accountId: string },
|
|
313
|
+
): Promise<void> {
|
|
314
|
+
const wanted = bodyField(await readJsonBody(req), 'enabled');
|
|
315
|
+
if (typeof wanted !== 'boolean') throw new ApiError('enabled must be true or false', 400);
|
|
316
|
+
const enabled = await deps.setAccountEnabled(session.subject, agentId, target.station, target.accountId, wanted);
|
|
317
|
+
log.info({ agentId, station: target.station, account: target.accountId, enabled }, 'account-api: account enabled flag set');
|
|
318
|
+
sendJson(req, res, 200, {
|
|
319
|
+
agentId,
|
|
320
|
+
station: target.station,
|
|
321
|
+
accountId: target.accountId,
|
|
322
|
+
enabled,
|
|
323
|
+
activated: await activate(deps, target.station),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
297
327
|
async function handleSession(
|
|
298
328
|
req: IncomingMessage,
|
|
299
329
|
res: ServerResponse,
|
|
@@ -345,6 +375,7 @@ async function dispatchRoute(
|
|
|
345
375
|
if (route.kind === 'step')
|
|
346
376
|
return handleStep(req, res, deps, ownerOf(session, agentId), route.attachId);
|
|
347
377
|
if (route.kind === 'allowlist') return handleAllowlist(req, res, deps, session, agentId, route);
|
|
378
|
+
if (route.kind === 'enabled') return handleEnabled(req, res, deps, session, agentId, route);
|
|
348
379
|
if (route.kind === 'senders') {
|
|
349
380
|
sendJson(req, res, 200, { senders: deps.recentSenders(route.station, route.accountId) });
|
|
350
381
|
return;
|
|
@@ -47,7 +47,7 @@ function stationOf(raw: unknown): LoadedAccount {
|
|
|
47
47
|
const allowlist = Array.isArray(raw.allowlist)
|
|
48
48
|
? raw.allowlist.filter((s): s is string => typeof s === 'string')
|
|
49
49
|
: null;
|
|
50
|
-
return { station: raw.station as LoadedAccount['station'], id: raw.id, allowlist, config: raw.config };
|
|
50
|
+
return { station: raw.station as LoadedAccount['station'], id: raw.id, allowlist, enabled: raw.enabled !== false, config: raw.config };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function connectorOf(raw: unknown): LoadedConnector {
|
|
@@ -305,7 +305,7 @@ export async function localAttachAccount(
|
|
|
305
305
|
if (typeof token === 'string') assertTokenFree(storedAgents(dir), station, token);
|
|
306
306
|
const taken = new Set(stored.file.stations.map((a) => a.id));
|
|
307
307
|
const accountId = freshId(taken);
|
|
308
|
-
stored.file.stations.push({ station, id: accountId, allowlist: ['*'], config });
|
|
308
|
+
stored.file.stations.push({ station, id: accountId, allowlist: ['*'], enabled: true, config });
|
|
309
309
|
save(stored);
|
|
310
310
|
return Promise.resolve({ agentId, station, accountId });
|
|
311
311
|
}
|
|
@@ -326,6 +326,22 @@ export async function localSetAllowlist(
|
|
|
326
326
|
return Promise.resolve(allowlist);
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
export async function localSetAccountEnabled(
|
|
330
|
+
subject: string,
|
|
331
|
+
agentId: string,
|
|
332
|
+
station: StationName,
|
|
333
|
+
accountId: string,
|
|
334
|
+
enabled: boolean,
|
|
335
|
+
dir = agentsDir(),
|
|
336
|
+
): Promise<boolean> {
|
|
337
|
+
const stored = ownedOrThrow(subject, agentId, dir);
|
|
338
|
+
const account = stored.file.stations.find((a) => a.station === station && a.id === accountId);
|
|
339
|
+
if (account === undefined) throw new AgentAdminError('no such account on this agent', 404);
|
|
340
|
+
account.enabled = enabled;
|
|
341
|
+
save(stored);
|
|
342
|
+
return Promise.resolve(enabled);
|
|
343
|
+
}
|
|
344
|
+
|
|
329
345
|
export async function localDetachAccount(
|
|
330
346
|
subject: string,
|
|
331
347
|
agentId: string,
|
|
@@ -56,7 +56,7 @@ function stationOf(raw: unknown, path: string, index: number): LoadedAccount {
|
|
|
56
56
|
if (typeof id !== 'string' || !ID_RE.test(id))
|
|
57
57
|
fail(path, `${where}.id is not an 11-character id`);
|
|
58
58
|
if (!isRecord(config)) fail(path, `${where}.config is not an object`);
|
|
59
|
-
return { station, id, allowlist: allowlistOf(raw.allowlist, path, where), config };
|
|
59
|
+
return { station, id, allowlist: allowlistOf(raw.allowlist, path, where), enabled: raw.enabled !== false, config };
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function optionalMatch(
|
|
@@ -8,6 +8,7 @@ const mapKey = (station: string, accountId: string): string =>
|
|
|
8
8
|
let agentMap: AgentMap = {};
|
|
9
9
|
let agentNames: AgentNameMap = {};
|
|
10
10
|
let allowlistMap: AllowlistMap = {};
|
|
11
|
+
let disabledAccounts = new Set<string>();
|
|
11
12
|
|
|
12
13
|
export function setAgentMap(map: AgentMap, names: AgentNameMap): void {
|
|
13
14
|
agentMap = map;
|
|
@@ -18,6 +19,12 @@ export function setAllowlistMap(map: AllowlistMap): void {
|
|
|
18
19
|
allowlistMap = map;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
export function setDisabledAccounts(ids: Set<string>): void {
|
|
23
|
+
disabledAccounts = ids;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const accountEnabled = (station: string, accountId: string): boolean => !disabledAccounts.has(mapKey(station, accountId));
|
|
27
|
+
|
|
21
28
|
export function accountFromLine(
|
|
22
29
|
line: string,
|
|
23
30
|
): { station: string; accountId: string } | undefined {
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from '../stations/registry.js';
|
|
6
6
|
import { listEndpoints } from '../net/tunnel.js';
|
|
7
7
|
import { hookUrl } from '../stations/attach.js';
|
|
8
|
-
import { agentIdForAccount, allowlistForAccount, knownAccounts, type KnownAccount } from '../agents/map.js';
|
|
8
|
+
import { accountEnabled, agentIdForAccount, allowlistForAccount, knownAccounts, type KnownAccount } from '../agents/map.js';
|
|
9
9
|
|
|
10
10
|
const accountId = (acc: unknown): string | undefined => {
|
|
11
11
|
const id = (acc as { id?: unknown }).id;
|
|
@@ -25,7 +25,8 @@ function withAgentId(station: string, acc: unknown): unknown {
|
|
|
25
25
|
const agentId = agentIdForAccount(station, id);
|
|
26
26
|
if (agentId === undefined) return acc;
|
|
27
27
|
const allowlist = allowlistForAccount(station, id);
|
|
28
|
-
|
|
28
|
+
const tagged = allowlist === undefined ? { ...rec, agentId } : { ...rec, agentId, allowlist };
|
|
29
|
+
return accountEnabled(station, id) ? tagged : { ...tagged, enabled: false };
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export function attachAgentIds(
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
localDeleteAgent,
|
|
37
37
|
localDetachAccount,
|
|
38
38
|
localSetAllowlist,
|
|
39
|
+
localSetAccountEnabled,
|
|
39
40
|
localImportAgent,
|
|
40
41
|
localListAgents,
|
|
41
42
|
localOwnedAgentOrThrow,
|
|
@@ -103,6 +104,7 @@ function agentApi(deps: LocalModeDeps): AgentApiDeps {
|
|
|
103
104
|
detachAccount: localDetachAccount,
|
|
104
105
|
syncStations: deps.syncStations,
|
|
105
106
|
setAllowlist: localSetAllowlist,
|
|
107
|
+
setAccountEnabled: localSetAccountEnabled,
|
|
106
108
|
recentSenders,
|
|
107
109
|
reloadAgents: deps.reloadAgents,
|
|
108
110
|
};
|
|
@@ -13,6 +13,7 @@ import { trainsDir } from '../boot/paths.js';
|
|
|
13
13
|
import {
|
|
14
14
|
setAgentMap,
|
|
15
15
|
setAllowlistMap,
|
|
16
|
+
setDisabledAccounts,
|
|
16
17
|
type AgentMap,
|
|
17
18
|
type AgentNameMap,
|
|
18
19
|
type AllowlistMap,
|
|
@@ -30,6 +31,7 @@ export interface LoadedAccount {
|
|
|
30
31
|
station: StationName;
|
|
31
32
|
id: string;
|
|
32
33
|
allowlist: string[] | null;
|
|
34
|
+
enabled?: boolean;
|
|
33
35
|
config: Record<string, unknown>;
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -126,6 +128,7 @@ function writeStations(list: LoadedAgent[]): WrittenStations {
|
|
|
126
128
|
const map: AgentMap = {};
|
|
127
129
|
const names: AgentNameMap = {};
|
|
128
130
|
const allow: AllowlistMap = {};
|
|
131
|
+
const disabled = new Set<string>();
|
|
129
132
|
for (const agent of list) {
|
|
130
133
|
names[agent.id] = agent.name;
|
|
131
134
|
for (const a of agent.accounts) {
|
|
@@ -138,6 +141,10 @@ function writeStations(list: LoadedAgent[]): WrittenStations {
|
|
|
138
141
|
}
|
|
139
142
|
map[`${a.station}/${a.id}`] = agent.id;
|
|
140
143
|
if (a.allowlist) allow[`${a.station}/${a.id}`] = a.allowlist;
|
|
144
|
+
if (a.enabled === false) {
|
|
145
|
+
disabled.add(`${a.station}/${a.id}`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
141
148
|
const cur = byStation.get(a.station);
|
|
142
149
|
if (cur) cur.push(a);
|
|
143
150
|
else byStation.set(a.station, [a]);
|
|
@@ -145,6 +152,7 @@ function writeStations(list: LoadedAgent[]): WrittenStations {
|
|
|
145
152
|
}
|
|
146
153
|
setAgentMap(map, names);
|
|
147
154
|
setAllowlistMap(allow);
|
|
155
|
+
setDisabledAccounts(disabled);
|
|
148
156
|
|
|
149
157
|
const active = new Map<StationName, number>();
|
|
150
158
|
const changed: StationName[] = [];
|
package/runtime/runtime.json
CHANGED