@stage-labs/metro 0.1.0-beta.172 → 0.1.0-beta.173

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.
Files changed (134) hide show
  1. package/package.json +1 -1
  2. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
  3. package/runtime/node_modules/@metro-labs/core/package.json +4 -1
  4. package/runtime/node_modules/@metro-labs/core/src/events.ts +1 -3
  5. package/runtime/node_modules/@metro-labs/core/src/ids.ts +3 -0
  6. package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +17 -0
  7. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +0 -1
  8. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +2 -5
  9. package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +42 -0
  10. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +89 -0
  11. package/runtime/node_modules/@metro-labs/core/src/tickets.ts +37 -0
  12. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +11 -28
  13. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +0 -9
  14. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +11 -91
  15. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +7 -8
  16. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +14 -90
  17. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -34
  18. package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +0 -5
  19. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +4 -7
  20. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +1 -13
  21. package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +1 -1
  22. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +5 -9
  23. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -7
  24. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -16
  25. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +3 -20
  26. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +15 -26
  27. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -10
  28. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +26 -55
  29. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +1 -8
  30. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +1 -5
  31. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +8 -21
  32. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +20 -9
  33. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +4 -14
  34. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +75 -64
  35. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +4 -32
  36. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +2 -24
  37. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +25 -20
  38. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +3 -1
  39. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +3 -19
  40. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +15 -41
  41. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +17 -40
  42. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +5 -5
  43. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +17 -132
  44. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +9 -18
  45. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +35 -140
  46. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +43 -2
  47. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +118 -0
  48. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +5 -8
  49. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +18 -40
  50. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +15 -15
  51. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +14 -26
  52. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +12 -21
  53. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +30 -133
  54. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +0 -1
  55. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +0 -1
  56. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +4 -4
  57. package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +36 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +5 -5
  59. package/runtime/node_modules/@metro-labs/daemon/src/gateway/subscription.ts +120 -0
  60. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +15 -0
  61. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +1 -1
  62. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +5 -0
  63. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +14 -15
  64. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +22 -71
  65. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +4 -5
  66. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +5 -6
  67. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +48 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +12 -31
  69. package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +1 -6
  70. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -75
  71. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +3 -8
  72. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +19 -29
  73. package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +1 -1
  74. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +15 -41
  75. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +3 -25
  76. package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +3 -25
  77. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +7 -34
  78. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +1 -6
  79. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +1 -13
  80. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +51 -0
  81. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +8 -31
  82. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +6 -31
  83. package/runtime/node_modules/@metro-labs/discord-bot/package.json +1 -3
  84. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +0 -1
  85. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +0 -8
  86. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +7 -66
  87. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +5 -10
  88. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -4
  89. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +39 -7
  90. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +0 -1
  91. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +0 -48
  92. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +11 -33
  93. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +4 -43
  94. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +0 -1
  95. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +0 -1
  96. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +0 -8
  97. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +9 -50
  98. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +7 -13
  99. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +3 -2
  100. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -2
  101. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -1
  102. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +0 -1
  103. package/runtime/node_modules/@metro-labs/threema/src/files.ts +8 -43
  104. package/runtime/node_modules/@metro-labs/threema/src/format.ts +3 -16
  105. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +5 -6
  106. package/runtime/node_modules/@metro-labs/threema/src/index.ts +2 -7
  107. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +0 -1
  108. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -56
  109. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +10 -27
  110. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +4 -37
  111. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +0 -1
  112. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -4
  113. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -2
  114. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +0 -27
  115. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +0 -10
  116. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +4 -23
  117. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -5
  118. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +4 -8
  119. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +0 -47
  120. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +23 -43
  121. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +3 -5
  122. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
  123. package/runtime/runtime.json +1 -1
  124. package/runtime/stations.json +1 -3
  125. package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +0 -69
  126. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +0 -65
  127. package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +0 -29
  128. package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +0 -21
  129. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +0 -172
  130. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +0 -43
  131. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +0 -195
  132. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +0 -292
  133. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +0 -197
  134. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +0 -63
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.172",
3
+ "version": "0.1.0-beta.173",
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.172",
3
+ "version": "0.1.0-beta.173",
4
4
  "description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
5
5
  "author": {
6
6
  "name": "Bonustrack",
@@ -14,6 +14,7 @@
14
14
  "./train-error": "./src/train-error.ts",
15
15
  "./trains/protocol": "./src/protocol.ts",
16
16
  "./ids": "./src/ids.ts",
17
+ "./tickets": "./src/tickets.ts",
17
18
  "./address": "./src/address.ts",
18
19
  "./station-names": "./src/station-names.ts",
19
20
  "./str": "./src/str.ts",
@@ -25,7 +26,9 @@
25
26
  "./stations/attachments": "./src/stations/attachments.ts",
26
27
  "./stations/messaging-normalize": "./src/stations/messaging-normalize.ts",
27
28
  "./stations/profile": "./src/stations/profile.ts",
28
- "./stations/sender-profile": "./src/stations/sender-profile.ts"
29
+ "./stations/sender-profile": "./src/stations/sender-profile.ts",
30
+ "./stations/train-events": "./src/stations/train-events.ts",
31
+ "./stations/train-boot": "./src/stations/train-boot.ts"
29
32
  },
30
33
  "scripts": {
31
34
  "build": "tsc",
@@ -1,4 +1,3 @@
1
- import { randomBytes } from 'node:crypto';
2
1
  import { Line } from './lines.js';
3
2
 
4
3
  export { userSelf, daemonSelf } from './identity.js';
@@ -74,8 +73,7 @@ export function formatDisplay(e: MetroEvent): string {
74
73
  return `${headerFor('📩', [e.station, e.fromName ?? e.from, e.lineName])}\n> ${body}`;
75
74
  }
76
75
 
77
- export const mintId = (): string =>
78
- `msg_${randomBytes(6).toString('base64url')}`;
76
+ export { mintId } from './ids.js';
79
77
 
80
78
  export type BusListener = (event: MetroEvent, busSeq: number) => void;
81
79
 
@@ -5,6 +5,9 @@ const REROLLS = 16;
5
5
 
6
6
  export const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{10}$/;
7
7
 
8
+ export const mintId = (): string =>
9
+ `msg_${randomBytes(6).toString('base64url')}`;
10
+
8
11
  export function newId(): string {
9
12
  for (let attempt = 0; attempt < REROLLS; attempt += 1) {
10
13
  const id = randomBytes(ID_BYTES).toString('base64url');
@@ -38,6 +38,23 @@ export function writeSecure(path: string, data: string): void {
38
38
  chmodIfExists(path, 0o600);
39
39
  }
40
40
 
41
+ function modeOf(path: string): number | null {
42
+ try {
43
+ return statSync(path).mode & 0o777;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ export function writeAtomic(path: string, data: string, mode?: number): void {
50
+ const chosen = mode ?? modeOf(path) ?? 0o644;
51
+ mkdirSync(dirname(path), { recursive: true });
52
+ const tmp = `${path}.metro-${String(process.pid)}.tmp`;
53
+ writeFileSync(tmp, data, { mode: chosen });
54
+ chmodSync(tmp, chosen);
55
+ renameSync(tmp, path);
56
+ }
57
+
41
58
  export function readJson<T>(
42
59
  path: string,
43
60
  fallback: T,
@@ -10,4 +10,3 @@ export const STATIONS = [
10
10
 
11
11
  export type StationName = (typeof STATIONS)[number];
12
12
 
13
- export type ConnectorTransport = 'http' | 'sse';
@@ -12,8 +12,7 @@ export const respond = (
12
12
  JSON.stringify({ op: 'response', id, ...body }) + '\n',
13
13
  );
14
14
 
15
- export const mintId = (): string =>
16
- `msg_${Math.random().toString(36).slice(2, 10)}`;
15
+ export { mintId } from '../ids.js';
17
16
 
18
17
  type Args = Record<string, unknown>;
19
18
 
@@ -29,13 +28,12 @@ export type StationHandler = (id: string, args: Args) => void | Promise<void>;
29
28
  export interface StationConfig {
30
29
  handlers: Record<string, StationHandler>;
31
30
  normalize: (action: string, args: Args) => Normalized;
32
- preDispatch?: (id: string, action: string) => boolean;
33
31
  }
34
32
 
35
33
  const lineTag = (args: Args): string =>
36
34
  typeof args.line === 'string' ? args.line : '?';
37
35
 
38
- export function makeStation({ handlers, normalize, preDispatch }: StationConfig) {
36
+ export function makeStation({ handlers, normalize }: StationConfig) {
39
37
  const known = Object.keys(handlers).join(', ');
40
38
  return async function handleCall(msg: CallMsg): Promise<void> {
41
39
  const { id } = msg;
@@ -44,7 +42,6 @@ export function makeStation({ handlers, normalize, preDispatch }: StationConfig)
44
42
  try {
45
43
  ({ action, args } = normalize(msg.action, msg.args));
46
44
  emit({ op: 'log', text: `call ${action} recv (line=${lineTag(args)})` });
47
- if (preDispatch?.(id, action)) return;
48
45
  const handler = handlers[action];
49
46
  if (!handler) {
50
47
  respond(id, { error: `unknown action '${action}' (have: ${known})` });
@@ -0,0 +1,42 @@
1
+ import { errMsg } from '../log.js';
2
+ import { readCalls } from '../protocol.js';
3
+ import { TrainError } from '../train-error.js';
4
+ import type { CallMsg } from './station-runtime.js';
5
+
6
+ export function announceAccounts(station: string, ids: Iterable<string>): void {
7
+ const list = [...ids];
8
+ if (list.length === 0) {
9
+ process.stderr.write(`${station}: no accounts booted, exiting\n`);
10
+ process.exit(2);
11
+ }
12
+ process.stderr.write(`${station} train ready — ${String(list.length)} account(s): ${list.join(', ')}\n`);
13
+ }
14
+
15
+ export interface ClientTrain<A extends { id: string }, C> {
16
+ station: string;
17
+ accounts: Map<string, A>;
18
+ loadAccounts: () => A[];
19
+ createClient: (account: A) => C;
20
+ startInbound: (client: C) => Promise<void>;
21
+ makeHandleCall: (clientFor: (accountId: string) => C) => (msg: CallMsg) => Promise<void>;
22
+ }
23
+
24
+ export function runClientTrain<A extends { id: string }, C>(train: ClientTrain<A, C>): void {
25
+ const clients = new Map<string, C>();
26
+ const clientFor = (accountId: string): C => {
27
+ const known = clients.get(accountId);
28
+ if (known !== undefined) return known;
29
+ const account = train.accounts.get(accountId);
30
+ if (!account) throw new TrainError('not_implemented', `unknown account '${accountId}'`);
31
+ const client = train.createClient(account);
32
+ clients.set(accountId, client);
33
+ return client;
34
+ };
35
+ readCalls(train.station, train.makeHandleCall(clientFor));
36
+ for (const cfg of train.loadAccounts()) train.accounts.set(cfg.id, cfg);
37
+ for (const id of train.accounts.keys())
38
+ train.startInbound(clientFor(id)).catch((err: unknown) => {
39
+ process.stderr.write(`${train.station}[${id}] inbound failed: ${errMsg(err)}\n`);
40
+ });
41
+ announceAccounts(train.station, train.accounts.keys());
42
+ }
@@ -0,0 +1,89 @@
1
+ import { mintId } from '../ids.js';
2
+ import { emit } from './station-runtime.js';
3
+
4
+ type Fields = Record<string, unknown>;
5
+ type Out = (e: unknown) => void;
6
+
7
+ export const selfUri = (station: string, account?: string): string =>
8
+ process.env.METRO_SELF_URI ??
9
+ (account === undefined ? `metro://${station}/self` : `metro://${station}/${account}/self`);
10
+
11
+ export function emitInbound(account: string, e: Fields, out: Out = emit): void {
12
+ out({
13
+ kind: 'inbound',
14
+ ...e,
15
+ account,
16
+ payload: { ...(e.payload as Fields | undefined), account },
17
+ });
18
+ }
19
+
20
+ export interface AttachmentAt {
21
+ station: string;
22
+ account: string;
23
+ line: string;
24
+ forId: string;
25
+ index: number;
26
+ from?: string;
27
+ }
28
+
29
+ export interface SavedFile {
30
+ path: string;
31
+ mime?: string;
32
+ name?: string;
33
+ }
34
+
35
+ function attachmentEvent(at: AttachmentAt, text: string, payload: Fields): Fields {
36
+ return {
37
+ kind: 'inbound',
38
+ id: mintId(),
39
+ ts: new Date().toISOString(),
40
+ station: at.station,
41
+ line: at.line,
42
+ from: at.from ?? selfUri(at.station, at.account),
43
+ text,
44
+ account: at.account,
45
+ payload: { account: at.account, attachmentFor: at.forId, index: at.index, ...payload },
46
+ };
47
+ }
48
+
49
+ export const attachmentSavedEvent = (at: AttachmentAt & { saved: SavedFile; extra?: Fields }): Fields =>
50
+ attachmentEvent(at, `📎 saved: ${at.saved.path}`, {
51
+ contentType: 'attachmentSaved',
52
+ ...at.extra,
53
+ attachmentPath: at.saved.path,
54
+ localPath: at.saved.path,
55
+ mime: at.saved.mime,
56
+ name: at.saved.name,
57
+ });
58
+
59
+ export const attachmentFailedEvent = (at: AttachmentAt & { reason: string; extra?: Fields }): Fields =>
60
+ attachmentEvent(at, `📎 not fetched: ${at.reason}`, {
61
+ contentType: 'attachmentFailed',
62
+ ...at.extra,
63
+ reason: at.reason,
64
+ });
65
+
66
+ const reasonOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));
67
+
68
+ export function reportAttachment(
69
+ save: Promise<SavedFile>,
70
+ at: AttachmentAt,
71
+ extra: { saved?: Fields; failed?: Fields } = {},
72
+ out: Out = emit,
73
+ ): void {
74
+ const tag = `${at.station}[${at.account}]`;
75
+ save
76
+ .then(
77
+ (saved) => {
78
+ out(attachmentSavedEvent({ ...at, saved, extra: extra.saved }));
79
+ },
80
+ (err: unknown) => {
81
+ const reason = reasonOf(err);
82
+ process.stderr.write(`${tag} attachment save failed: ${reason}\n`);
83
+ out(attachmentFailedEvent({ ...at, reason, extra: extra.failed }));
84
+ },
85
+ )
86
+ .catch((err: unknown) => {
87
+ process.stderr.write(`${tag} attachment event not emitted: ${reasonOf(err)}\n`);
88
+ });
89
+ }
@@ -0,0 +1,37 @@
1
+ import { randomBytes } from 'node:crypto';
2
+
3
+ export interface TicketStore<T> {
4
+ mint: (value: T, now?: number) => { ticket: string; expiresAt: number };
5
+ take: (ticket: string, now?: number) => T | undefined;
6
+ size: (now?: number) => number;
7
+ }
8
+
9
+ export function ticketStore<T>(ttlMs: number, max: number): TicketStore<T> {
10
+ const open = new Map<string, { value: T; expiresAt: number }>();
11
+ const prune = (now: number): void => {
12
+ for (const [ticket, entry] of open) if (entry.expiresAt <= now) open.delete(ticket);
13
+ };
14
+ return {
15
+ mint(value, now = Date.now()) {
16
+ prune(now);
17
+ while (open.size >= max) {
18
+ const oldest = open.keys().next();
19
+ if (oldest.done === true) break;
20
+ open.delete(oldest.value);
21
+ }
22
+ const ticket = randomBytes(32).toString('base64url');
23
+ const expiresAt = now + ttlMs;
24
+ open.set(ticket, { value, expiresAt });
25
+ return { ticket, expiresAt };
26
+ },
27
+ take(ticket, now = Date.now()) {
28
+ const entry = open.get(ticket);
29
+ open.delete(ticket);
30
+ return entry !== undefined && entry.expiresAt > now ? entry.value : undefined;
31
+ },
32
+ size(now = Date.now()) {
33
+ prune(now);
34
+ return open.size;
35
+ },
36
+ };
37
+ }
@@ -8,7 +8,6 @@ import {
8
8
  bodyField,
9
9
  readJsonBody,
10
10
  sendJson,
11
- type ApiSession,
12
11
  } from '@metro-labs/http/api-http';
13
12
  import { type AccountRef } from './account-attach.js';
14
13
  import { type AccountRoute } from './account-routes.js';
@@ -51,13 +50,11 @@ export interface AccountApiDeps {
51
50
  attachSessions: AttachSessionApi;
52
51
  prepareAccount: (input: AttachInput) => Promise<PreparedAccount>;
53
52
  attachAccount: (
54
- subject: string,
55
53
  agentId: string,
56
54
  station: StationName,
57
55
  config: Record<string, unknown>,
58
56
  ) => Promise<AccountRef>;
59
57
  detachAccount: (
60
- subject: string,
61
58
  agentId: string,
62
59
  station: StationName,
63
60
  accountId: string,
@@ -65,7 +62,6 @@ export interface AccountApiDeps {
65
62
  syncStations: (station: StationName) => Promise<void>;
66
63
  reloadAgents: () => Promise<void>;
67
64
  setAllowlist: (
68
- subject: string,
69
65
  agentId: string,
70
66
  station: StationName,
71
67
  accountId: string,
@@ -83,7 +79,6 @@ export interface AccountApiDeps {
83
79
  args: Record<string, unknown>,
84
80
  ) => Promise<unknown>;
85
81
  setAccountEnabled: (
86
- subject: string,
87
82
  agentId: string,
88
83
  station: StationName,
89
84
  accountId: string,
@@ -160,14 +155,12 @@ interface AttachPayload {
160
155
 
161
156
  async function storeAccount(
162
157
  deps: AccountApiDeps,
163
- session: ApiSession,
164
158
  agentId: string,
165
159
  station: StationName,
166
160
  prepared: PreparedAccount,
167
161
  ): Promise<AccountRef> {
168
162
  try {
169
163
  return await deps.attachAccount(
170
- session.subject,
171
164
  agentId,
172
165
  station,
173
166
  prepared.config,
@@ -178,9 +171,6 @@ async function storeAccount(
178
171
  }
179
172
  }
180
173
 
181
- function ownerOf(session: ApiSession, agentId: string): AttachOwner {
182
- return { subject: session.subject, agentId };
183
- }
184
174
 
185
175
  function asInput(body: unknown): Record<string, unknown> {
186
176
  const out: Record<string, unknown> = {};
@@ -193,14 +183,13 @@ async function handleStart(
193
183
  req: IncomingMessage,
194
184
  res: ServerResponse,
195
185
  deps: AccountApiDeps,
196
- session: ApiSession,
197
186
  agentId: string,
198
187
  ): Promise<void> {
199
188
  const body = await readJsonBody(req);
200
189
  const station = bodyField(body, 'station');
201
190
  if (isInteractiveStation(station)) {
202
191
  const view = await deps.attachSessions.start(
203
- ownerOf(session, agentId),
192
+ { agentId },
204
193
  station,
205
194
  asInput(body),
206
195
  );
@@ -210,7 +199,7 @@ async function handleStart(
210
199
  if (!isAttachStation(station))
211
200
  throw new ApiError(`station must be one of ${ATTACHABLE.join(', ')}`, 400);
212
201
  const prepared = await deps.prepareAccount(attachInputOf(station, body));
213
- const ref = await storeAccount(deps, session, agentId, station, prepared);
202
+ const ref = await storeAccount(deps, agentId, station, prepared);
214
203
  log.info(
215
204
  { agentId: ref.agentId, station, account: ref.accountId },
216
205
  'account-api: attached a station account',
@@ -237,12 +226,10 @@ async function handleDetach(
237
226
  req: IncomingMessage,
238
227
  res: ServerResponse,
239
228
  deps: AccountApiDeps,
240
- session: ApiSession,
241
229
  agentId: string,
242
230
  target: { station: StationName; accountId: string },
243
231
  ): Promise<void> {
244
232
  const ref = await deps.detachAccount(
245
- session.subject,
246
233
  agentId,
247
234
  target.station,
248
235
  target.accountId,
@@ -274,12 +261,11 @@ async function handleAllowlist(
274
261
  req: IncomingMessage,
275
262
  res: ServerResponse,
276
263
  deps: AccountApiDeps,
277
- session: ApiSession,
278
264
  agentId: string,
279
265
  target: { station: StationName; accountId: string },
280
266
  ): Promise<void> {
281
267
  const wanted = normalizeAllowlist(bodyField(await readJsonBody(req), 'allowlist'));
282
- const allowlist = await deps.setAllowlist(session.subject, agentId, target.station, target.accountId, wanted);
268
+ const allowlist = await deps.setAllowlist(agentId, target.station, target.accountId, wanted);
283
269
  log.info({ agentId, station: target.station, account: target.accountId, senders: allowlist.length }, 'account-api: allowlist set');
284
270
  sendJson(req, res, 200, {
285
271
  agentId,
@@ -294,13 +280,12 @@ async function handleEnabled(
294
280
  req: IncomingMessage,
295
281
  res: ServerResponse,
296
282
  deps: AccountApiDeps,
297
- session: ApiSession,
298
283
  agentId: string,
299
284
  target: { station: StationName; accountId: string },
300
285
  ): Promise<void> {
301
286
  const wanted = bodyField(await readJsonBody(req), 'enabled');
302
287
  if (typeof wanted !== 'boolean') throw new ApiError('enabled must be true or false', 400);
303
- const enabled = await deps.setAccountEnabled(session.subject, agentId, target.station, target.accountId, wanted);
288
+ const enabled = await deps.setAccountEnabled(agentId, target.station, target.accountId, wanted);
304
289
  log.info({ agentId, station: target.station, account: target.accountId, enabled }, 'account-api: account enabled flag set');
305
290
  sendJson(req, res, 200, {
306
291
  agentId,
@@ -345,43 +330,41 @@ async function dispatchRoute(
345
330
  req: IncomingMessage,
346
331
  res: ServerResponse,
347
332
  deps: AccountApiDeps,
348
- session: ApiSession,
349
333
  agentId: string,
350
334
  route: AccountRoute,
351
335
  ): Promise<void> {
352
336
  if (route.kind === 'start')
353
- return handleStart(req, res, deps, session, agentId);
337
+ return handleStart(req, res, deps, agentId);
354
338
  if (route.kind === 'session')
355
339
  return handleSession(
356
340
  req,
357
341
  res,
358
342
  deps,
359
- ownerOf(session, agentId),
343
+ { agentId },
360
344
  route.attachId,
361
345
  );
362
346
  if (route.kind === 'step')
363
- return handleStep(req, res, deps, ownerOf(session, agentId), route.attachId);
364
- if (route.kind === 'allowlist') return handleAllowlist(req, res, deps, session, agentId, route);
365
- if (route.kind === 'enabled') return handleEnabled(req, res, deps, session, agentId, route);
347
+ return handleStep(req, res, deps, { agentId }, route.attachId);
348
+ if (route.kind === 'allowlist') return handleAllowlist(req, res, deps, agentId, route);
349
+ if (route.kind === 'enabled') return handleEnabled(req, res, deps, agentId, route);
366
350
  if (route.kind === 'resolve') return handleResolve(req, res, deps, route);
367
351
  if (route.kind === 'name') return handleName(req, res, deps, route);
368
352
  if (route.kind === 'senders') {
369
353
  sendJson(req, res, 200, { senders: deps.recentSenders(route.station, route.accountId) });
370
354
  return;
371
355
  }
372
- return handleDetach(req, res, deps, session, agentId, route);
356
+ return handleDetach(req, res, deps, agentId, route);
373
357
  }
374
358
 
375
359
  export async function handleAccountRoute(
376
360
  req: IncomingMessage,
377
361
  res: ServerResponse,
378
362
  deps: AccountApiDeps,
379
- session: ApiSession,
380
363
  agentId: string,
381
364
  route: AccountRoute,
382
365
  ): Promise<void> {
383
366
  try {
384
- await dispatchRoute(req, res, deps, session, agentId, route);
367
+ await dispatchRoute(req, res, deps, agentId, route);
385
368
  } catch (err) {
386
369
  apiFailure(req, res, err);
387
370
  }
@@ -7,8 +7,6 @@ export class AgentAdminError extends ApiError {}
7
7
  export interface AgentSummary {
8
8
  id: string;
9
9
  name: string | null;
10
- owned: boolean;
11
- key: string | null;
12
10
  }
13
11
 
14
12
  export interface CreatedAgent {
@@ -17,13 +15,6 @@ export interface CreatedAgent {
17
15
  key: string;
18
16
  }
19
17
 
20
- export interface OwnedAgent {
21
- id: string;
22
- name: string | null;
23
- }
24
-
25
- export type DeletedAgent = OwnedAgent;
26
-
27
18
  export function normalizeAgentName(raw: unknown): string {
28
19
  const name = typeof raw === 'string' ? raw.trim() : '';
29
20
  if (!AGENT_NAME_RE.test(name))
@@ -1,13 +1,10 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { parseId } from '@metro-labs/core/ids';
3
- import { log } from '@metro-labs/core/log';
4
3
  import {
5
4
  apiFailure,
6
- apiSession, requireAdmin,
7
- projectParam,
5
+ apiSession,
8
6
  cors,
9
7
  sendJson,
10
- type ApiSession,
11
8
  } from '@metro-labs/http/api-http';
12
9
  import { ATTACHABLE, handleAccountRoute, type AccountApiDeps } from './accounts-api.js';
13
10
  import {
@@ -15,38 +12,28 @@ import {
15
12
  accountRouteAllows,
16
13
  type AccountRoute,
17
14
  } from './account-routes.js';
18
- import {
19
- type AgentSummary,
20
- type DeletedAgent,
21
- } from './admin.js';
15
+ import type { AgentSummary } from './admin.js';
22
16
 
23
17
  const PREFIX = '/api/agents';
24
18
 
25
19
  export interface AgentApiDeps extends AccountApiDeps {
26
- listAgents: (subject: string, project: string) => Promise<AgentSummary[]>;
27
- deleteAgent: (
28
- subject: string,
29
- id: string,
30
- ) => Promise<DeletedAgent>;
20
+ listAgents: () => Promise<AgentSummary[]>;
31
21
  gatherAccounts: (allowed: Set<string>) => Promise<{
32
22
  accounts: Record<string, unknown[]>;
33
23
  unavailable: string[];
34
24
  }>;
35
25
  capabilities: () => Record<string, string[]>;
36
26
  attachable?: string[];
37
- liveness: () => Map<string, { connected: boolean; lastSeenAt: number }>;
38
27
  connectorIds: (agentIds: string[]) => Promise<Map<string, string[]>>;
39
28
  }
40
29
 
41
30
  type Routable =
42
31
  | { kind: 'collection' }
43
- | { kind: 'agent'; id: string }
44
32
  | { kind: 'accounts'; id: string; route: AccountRoute };
45
33
 
46
34
  type Target = Routable | { kind: 'unknown' } | null;
47
35
 
48
36
  function subTarget(id: string, rest: string[]): Target {
49
- if (rest.length === 0) return { kind: 'agent', id };
50
37
  if (rest[0] !== 'accounts') return { kind: 'unknown' };
51
38
  const route = accountRoute(rest.slice(1));
52
39
  return route === null ? { kind: 'unknown' } : { kind: 'accounts', id, route };
@@ -62,30 +49,8 @@ export function target(path: string): Target {
62
49
  return id === null ? { kind: 'unknown' } : subTarget(id, segments.slice(1));
63
50
  }
64
51
 
65
- function livenessPayload(
66
- agent: AgentSummary,
67
- live: Map<string, { connected: boolean; lastSeenAt: number }>,
68
- ): Record<string, unknown> {
69
- const found = agent.owned ? live.get(agent.id) : undefined;
70
- if (found === undefined) return { connected: false, last_seen: null };
71
- return {
72
- connected: found.connected,
73
- last_seen: new Date(found.lastSeenAt).toISOString(),
74
- };
75
- }
76
-
77
- function agentPayload(
78
- agent: AgentSummary,
79
- live: Map<string, { connected: boolean; lastSeenAt: number }>,
80
- connectors: Map<string, string[]>,
81
- ): Record<string, unknown> {
82
- return {
83
- id: agent.id,
84
- name: agent.name,
85
- owned: agent.owned,
86
- connector_ids: connectors.get(agent.id) ?? [],
87
- ...livenessPayload(agent, live),
88
- };
52
+ function agentPayload(agent: AgentSummary, connectors: Map<string, string[]>): Record<string, unknown> {
53
+ return { id: agent.id, name: agent.name, connector_ids: connectors.get(agent.id) ?? [] };
89
54
  }
90
55
 
91
56
  function wantsAccounts(req: IncomingMessage): boolean {
@@ -97,18 +62,11 @@ async function handleList(
97
62
  req: IncomingMessage,
98
63
  res: ServerResponse,
99
64
  deps: AgentApiDeps,
100
- session: ApiSession,
101
65
  ): Promise<void> {
102
- const project = projectParam(req);
103
- if (project === null) {
104
- sendJson(req, res, 400, { error: 'a project is required' });
105
- return;
106
- }
107
- const list = await deps.listAgents(session.subject, project);
108
- const live = deps.liveness();
66
+ const list = await deps.listAgents();
109
67
  const connectors = await deps.connectorIds(list.map((a) => a.id));
110
68
  const base = {
111
- agents: list.map((a) => agentPayload(a, live, connectors)),
69
+ agents: list.map((a) => agentPayload(a, connectors)),
112
70
  capabilities: deps.capabilities(),
113
71
  attachable: deps.attachable ?? ATTACHABLE,
114
72
  };
@@ -122,48 +80,11 @@ async function handleList(
122
80
  sendJson(req, res, 200, { ...base, accounts, unavailable });
123
81
  }
124
82
 
125
- async function handleDelete(
126
- req: IncomingMessage,
127
- res: ServerResponse,
128
- deps: AgentApiDeps,
129
- session: ApiSession,
130
- id: string,
131
- ): Promise<void> {
132
- const gone = await deps.deleteAgent(session.subject, id);
133
- log.info(
134
- { agent: gone.name, id: gone.id, owner: session.subject },
135
- 'agent-api: deleted agent',
136
- );
137
- sendJson(req, res, 200, { id: gone.id, name: gone.name, deleted: true });
138
- }
139
-
140
- type AgentTarget = Exclude<Routable, { kind: 'accounts' }>;
141
-
142
- async function routeAgent(
143
- req: IncomingMessage,
144
- res: ServerResponse,
145
- deps: AgentApiDeps,
146
- session: ApiSession,
147
- tgt: AgentTarget,
148
- ): Promise<void> {
149
- try {
150
- if (tgt.kind === 'agent') {
151
- requireAdmin(session);
152
- await handleDelete(req, res, deps, session, tgt.id);
153
- } else await handleList(req, res, deps, session);
154
- } catch (err) {
155
- apiFailure(req, res, err);
156
- }
157
- }
158
-
159
- const ALLOWED: Record<AgentTarget['kind'], string[]> = {
160
- collection: ['GET'],
161
- agent: ['DELETE'],
162
- };
83
+ const ALLOWED_LIST = ['GET'];
163
84
 
164
85
  function methodAllowed(tgt: Routable, method: string | undefined): boolean {
165
86
  if (tgt.kind === 'accounts') return accountRouteAllows(tgt.route, method);
166
- return ALLOWED[tgt.kind].includes(method ?? '');
87
+ return ALLOWED_LIST.includes(method ?? '');
167
88
  }
168
89
 
169
90
  async function dispatch(
@@ -177,9 +98,8 @@ async function dispatch(
177
98
  sendJson(req, res, 401, { error: 'unauthorized' });
178
99
  return;
179
100
  }
180
- if (tgt.kind === 'accounts')
181
- await handleAccountRoute(req, res, deps, session, tgt.id, tgt.route);
182
- else await routeAgent(req, res, deps, session, tgt);
101
+ if (tgt.kind === 'accounts') await handleAccountRoute(req, res, deps, tgt.id, tgt.route);
102
+ else await handleList(req, res, deps);
183
103
  }
184
104
 
185
105
  export function handleAgentApiRequest(