@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.97",
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.97",
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
- return allowlist === undefined ? { ...rec, agentId } : { ...rec, agentId, allowlist };
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[] = [];
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.97"
2
+ "version": "0.1.0-beta.98"
3
3
  }