@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
@@ -23,7 +23,6 @@ export const loadedAgentOf = (bundle: AgentBundle): LoadedAgent => ({
23
23
  name: bundle.agent.name,
24
24
  key: null,
25
25
  accounts: bundle.agent.stations,
26
- connectors: [],
27
26
  });
28
27
 
29
28
  export interface RestoredAgent {
@@ -36,8 +35,8 @@ export interface RestoredAgent {
36
35
  export type ImportMode = 'append' | 'overwrite';
37
36
 
38
37
  export interface BundleApiDeps {
39
- bundle: (subject: string, agentId: string) => Promise<AgentBundle>;
40
- restore: (subject: string, bundle: AgentBundle, mode: ImportMode) => Promise<RestoredAgent>;
38
+ bundle: (agentId: string) => Promise<AgentBundle>;
39
+ restore: (bundle: AgentBundle, mode: ImportMode) => Promise<RestoredAgent>;
41
40
  }
42
41
 
43
42
  export function parseMode(raw: unknown): ImportMode {
@@ -62,7 +61,7 @@ function connectorOf(raw: unknown): LoadedConnector {
62
61
  if (!isRecord(raw) || typeof raw.id !== 'string' || typeof raw.name !== 'string' || typeof raw.url !== 'string')
63
62
  throw bad('a connector is missing its id, name or url');
64
63
  if (!isRecord(raw.config)) throw bad('a connector has no config');
65
- return { id: raw.id, name: raw.name, url: raw.url, transport: 'http', config: raw.config };
64
+ return { id: raw.id, name: raw.name, url: raw.url, config: raw.config };
66
65
  }
67
66
 
68
67
  function agentOf(raw: unknown): AgentBundle['agent'] {
@@ -79,14 +78,14 @@ export function parseBundle(raw: unknown): AgentBundle {
79
78
  return { version: 1, agent: agentOf(raw.agent), connectors: connectors.map(connectorOf) };
80
79
  }
81
80
 
82
- async function answer(req: IncomingMessage, deps: BundleApiDeps, subject: string, path: string): Promise<unknown> {
81
+ async function answer(req: IncomingMessage, deps: BundleApiDeps, path: string): Promise<unknown> {
83
82
  if (path === RESTORE_PATH) {
84
83
  const body: unknown = await readJsonBody(req, BUNDLE_MAX);
85
- return deps.restore(subject, parseBundle(body), parseMode(body));
84
+ return deps.restore(parseBundle(body), parseMode(body));
86
85
  }
87
86
  const id = parseId(path.slice(AGENTS.length + 1).split('/')[0] ?? '');
88
87
  if (id === null) throw new ApiError('no such agent', 404);
89
- return deps.bundle(subject, id);
88
+ return deps.bundle(id);
90
89
  }
91
90
 
92
91
  export function handleBundleRequest(req: IncomingMessage, res: ServerResponse, deps: BundleApiDeps): boolean {
@@ -106,7 +105,7 @@ export function handleBundleRequest(req: IncomingMessage, res: ServerResponse, d
106
105
  .then((session) => {
107
106
  if (!session) throw new ApiError('unauthorized', 401);
108
107
  requireAdmin(session);
109
- return answer(req, deps, session.subject, path);
108
+ return answer(req, deps, path);
110
109
  })
111
110
  .then((body) => {
112
111
  sendJson(req, res, isRestore ? 201 : 200, body);
@@ -1,4 +1,4 @@
1
- import { readFileSync, rmdirSync, rmSync } from 'node:fs';
1
+ import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ApiError } from '@metro-labs/http/api-error';
4
4
  import { isOrganizationId } from '@metro-labs/http/workos-token';
@@ -9,8 +9,6 @@ import {
9
9
  normalizeAgentName,
10
10
  type AgentSummary,
11
11
  type CreatedAgent,
12
- type DeletedAgent,
13
- type OwnedAgent,
14
12
  } from './admin.js';
15
13
  import type { AccountRef } from './account-attach.js';
16
14
  import {
@@ -23,12 +21,11 @@ import {
23
21
  type AgentFile,
24
22
  } from './files.js';
25
23
  import { newId } from '@metro-labs/core/ids';
26
- import { registerKey, unregisterAgentKey } from './keys.js';
24
+ import { registerKey } from './keys.js';
27
25
  import { MOVABLE_STATIONS, type LoadedAgent } from '../stations/materialize.js';
28
26
  import type { StationName } from '@metro-labs/core/station-names';
29
27
  import { normalizeAddress } from '@metro-labs/core/address';
30
28
 
31
- export const LOCAL_PROJECT_ID = 'localdaemon';
32
29
  const OWNER_FILE = '.owner';
33
30
 
34
31
  interface Stored {
@@ -59,12 +56,6 @@ export function setLocalOwner(raw: string, dir = agentsDir()): string {
59
56
  return owner;
60
57
  }
61
58
 
62
-
63
- function isOwner(subject: string, dir: string): boolean {
64
- const owner = localOwner(dir);
65
- return owner !== null && owner === parseOwner(subject);
66
- }
67
-
68
59
  export function storedAgents(dir: string): Stored[] {
69
60
  return listAgentFiles(dir).map((path) => ({ path, file: readAgentFile(path) }));
70
61
  }
@@ -73,34 +64,14 @@ function save(stored: Stored): void {
73
64
  writeSecure(stored.path, `${JSON.stringify(stored.file, null, 2)}\n`);
74
65
  }
75
66
 
76
- function ownedOrThrow(subject: string, id: string, dir: string): Stored {
77
- if (!isOwner(subject, dir)) throw missing();
67
+ function agentOrThrow(id: string, dir: string): Stored {
78
68
  const found = storedAgents(dir).find((s) => s.file.id === id);
79
69
  if (found === undefined) throw missing();
80
70
  return found;
81
71
  }
82
72
 
83
- export async function localOwnedAgentOrThrow(
84
- subject: string,
85
- id: string,
86
- dir = agentsDir(),
87
- ): Promise<{ agent: OwnedAgent }> {
88
- const { file } = ownedOrThrow(subject, id, dir);
89
- return Promise.resolve({ agent: { id: file.id, name: file.name } });
90
- }
91
-
92
- export async function localListAgents(
93
- subject: string,
94
- project: string,
95
- dir = agentsDir(),
96
- ): Promise<AgentSummary[]> {
97
- if (project !== LOCAL_PROJECT_ID || !isOwner(subject, dir))
98
- throw new AgentAdminError('no such project', 404);
99
- return Promise.resolve(
100
- storedAgents(dir)
101
- .map(({ file }) => ({ id: file.id, name: file.name, owned: true, key: file.key }))
102
- .sort((a, b) => a.id.localeCompare(b.id)),
103
- );
73
+ export async function localListAgents(dir = agentsDir()): Promise<AgentSummary[]> {
74
+ return Promise.resolve(storedAgents(dir).map(({ file }) => ({ id: file.id, name: file.name })));
104
75
  }
105
76
 
106
77
  function freshId(taken: Set<string>): string {
@@ -111,14 +82,7 @@ function freshId(taken: Set<string>): string {
111
82
  throw new AgentAdminError('could not allocate a free id', 500);
112
83
  }
113
84
 
114
- export async function localCreateAgent(
115
- subject: string,
116
- project: string,
117
- rawName?: string,
118
- dir = agentsDir(),
119
- ): Promise<CreatedAgent> {
120
- if (project !== LOCAL_PROJECT_ID || !isOwner(subject, dir))
121
- throw new AgentAdminError('no such project', 404);
85
+ export async function localCreateAgent(rawName?: string, dir = agentsDir()): Promise<CreatedAgent> {
122
86
  const name = rawName === undefined ? null : normalizeAgentName(rawName);
123
87
  const existing = storedAgents(dir);
124
88
  if (existing.length > 0) throw new AgentAdminError('this box already has its agent', 409);
@@ -127,7 +91,7 @@ export async function localCreateAgent(
127
91
  ensureSecureDir(dir);
128
92
  save({
129
93
  path: agentFilePath(dir),
130
- file: { version: 1, id, name, key, owner: localOwner(dir), stations: [], connectors: [] },
94
+ file: { version: 1, id, name, key, stations: [] },
131
95
  });
132
96
  registerKey(key, id);
133
97
  return Promise.resolve({ id, name, key });
@@ -137,30 +101,11 @@ export type Ensured = 'created' | 'present' | 'no-owner';
137
101
 
138
102
  export async function ensureLocalAgent(dir = agentsDir()): Promise<Ensured> {
139
103
  if (storedAgents(dir).length > 0) return 'present';
140
- const owner = localOwner(dir);
141
- if (owner === null) return 'no-owner';
142
- await localCreateAgent(owner, LOCAL_PROJECT_ID, undefined, dir);
104
+ if (localOwner(dir) === null) return 'no-owner';
105
+ await localCreateAgent(undefined, dir);
143
106
  return 'created';
144
107
  }
145
108
 
146
- export async function localDeleteAgent(
147
- subject: string,
148
- id: string,
149
- dir = agentsDir(),
150
- ): Promise<DeletedAgent> {
151
- const stored = ownedOrThrow(subject, id, dir);
152
- const attached = stored.file.stations.length;
153
- if (attached > 0)
154
- throw new AgentAdminError(
155
- `the agent still has ${String(attached)} station account(s) attached — detach them first`,
156
- 409,
157
- );
158
- rmSync(stored.path);
159
- removeIfEmpty(join(stored.path, '..'));
160
- unregisterAgentKey(id);
161
- return Promise.resolve({ id, name: stored.file.name });
162
- }
163
-
164
109
  function assertUnclaimed(existing: Stored[], agent: LoadedAgent): void {
165
110
  for (const { file } of existing) {
166
111
  if (file.id === agent.id)
@@ -170,10 +115,6 @@ function assertUnclaimed(existing: Stored[], agent: LoadedAgent): void {
170
115
  }
171
116
  }
172
117
 
173
- export function assertLocalOwner(subject: string, dir = agentsDir()): void {
174
- if (!isOwner(subject, dir)) throw new AgentAdminError('no such project', 404);
175
- }
176
-
177
118
  function assertImportable(agent: LoadedAgent): void {
178
119
  const immovable = agent.accounts.find((a) => !MOVABLE_STATIONS.has(a.station));
179
120
  if (immovable !== undefined)
@@ -200,16 +141,14 @@ function importTarget(dir: string, agent: LoadedAgent): ImportTarget {
200
141
  export type ImportMode = 'append' | 'overwrite';
201
142
 
202
143
  export async function localImportAgent(
203
- subject: string,
204
144
  agent: LoadedAgent,
205
145
  dir = agentsDir(),
206
146
  mode: ImportMode = 'overwrite',
207
147
  ): Promise<{ id: string; name: string; key: string; stations: number }> {
208
- assertLocalOwner(subject, dir);
209
148
  assertImportable(agent);
210
149
  const { path, previous } = importTarget(dir, agent);
211
150
  const key = previous?.key ?? agent.key ?? newApiKey();
212
- const file = fileFor({ ...agent, key }, localOwner(dir), path, previous, mode);
151
+ const file = fileFor({ ...agent, key }, path, previous, mode);
213
152
  ensureSecureDir(join(path, '..'));
214
153
  save({ path, file });
215
154
  registerKey(key, agent.id);
@@ -237,7 +176,6 @@ function mergedStations(
237
176
 
238
177
  function fileFor(
239
178
  agent: LoadedAgent,
240
- owner: string | null,
241
179
  path: string,
242
180
  previous: AgentFile | undefined,
243
181
  mode: ImportMode,
@@ -249,9 +187,7 @@ function fileFor(
249
187
  id: agent.id,
250
188
  name: previous?.name ?? null,
251
189
  key: agent.key,
252
- owner,
253
190
  stations: mergedStations(agent, previous, mode),
254
- connectors: previous?.connectors ?? [],
255
191
  }),
256
192
  path,
257
193
  );
@@ -268,14 +204,6 @@ export function readLocalAgentFile(agentId: string, dir = agentsDir()): AgentFil
268
204
  return found.file;
269
205
  }
270
206
 
271
- function removeIfEmpty(folder: string): void {
272
- try {
273
- rmdirSync(folder);
274
- } catch {
275
- return;
276
- }
277
- }
278
-
279
207
  function assertTokenFree(all: Stored[], station: StationName, token: string): void {
280
208
  const taken = all.some((s) =>
281
209
  s.file.stations.some(
@@ -290,13 +218,12 @@ function assertTokenFree(all: Stored[], station: StationName, token: string): vo
290
218
  }
291
219
 
292
220
  export async function localAttachAccount(
293
- subject: string,
294
221
  agentId: string,
295
222
  station: StationName,
296
223
  config: Record<string, unknown>,
297
224
  dir = agentsDir(),
298
225
  ): Promise<AccountRef> {
299
- const stored = ownedOrThrow(subject, agentId, dir);
226
+ const stored = agentOrThrow(agentId, dir);
300
227
  if (!MOVABLE_STATIONS.has(station))
301
228
  throw new AgentAdminError(
302
229
  `a ${station} endpoint needs a public url and cannot live on a local daemon`,
@@ -312,14 +239,13 @@ export async function localAttachAccount(
312
239
  }
313
240
 
314
241
  export async function localSetAllowlist(
315
- subject: string,
316
242
  agentId: string,
317
243
  station: StationName,
318
244
  accountId: string,
319
245
  allowlist: string[],
320
246
  dir = agentsDir(),
321
247
  ): Promise<string[]> {
322
- const stored = ownedOrThrow(subject, agentId, dir);
248
+ const stored = agentOrThrow(agentId, dir);
323
249
  const account = stored.file.stations.find((a) => a.station === station && a.id === accountId);
324
250
  if (account === undefined) throw new AgentAdminError('no such account on this agent', 404);
325
251
  account.allowlist = allowlist;
@@ -328,14 +254,13 @@ export async function localSetAllowlist(
328
254
  }
329
255
 
330
256
  export async function localSetAccountEnabled(
331
- subject: string,
332
257
  agentId: string,
333
258
  station: StationName,
334
259
  accountId: string,
335
260
  enabled: boolean,
336
261
  dir = agentsDir(),
337
262
  ): Promise<boolean> {
338
- const stored = ownedOrThrow(subject, agentId, dir);
263
+ const stored = agentOrThrow(agentId, dir);
339
264
  const account = stored.file.stations.find((a) => a.station === station && a.id === accountId);
340
265
  if (account === undefined) throw new AgentAdminError('no such account on this agent', 404);
341
266
  account.enabled = enabled;
@@ -344,13 +269,12 @@ export async function localSetAccountEnabled(
344
269
  }
345
270
 
346
271
  export async function localDetachAccount(
347
- subject: string,
348
272
  agentId: string,
349
273
  station: StationName,
350
274
  accountId: string,
351
275
  dir = agentsDir(),
352
276
  ): Promise<AccountRef> {
353
- const stored = ownedOrThrow(subject, agentId, dir);
277
+ const stored = agentOrThrow(agentId, dir);
354
278
  const before = stored.file.stations.length;
355
279
  stored.file.stations = stored.file.stations.filter(
356
280
  (a) => !(a.station === station && a.id === accountId),
@@ -19,9 +19,7 @@ export interface AgentFile {
19
19
  id: string;
20
20
  name: string | null;
21
21
  key: string | null;
22
- owner: string | null;
23
22
  stations: LoadedAccount[];
24
- connectors: string[];
25
23
  }
26
24
 
27
25
  export const agentFilePath = (dir = agentsDir()): string => join(dir, AGENT_FILE);
@@ -33,8 +31,6 @@ export function agentsDir(): string {
33
31
  : join(homedir(), '.metro', 'agents');
34
32
  }
35
33
 
36
- const OWNER_RE = /^(0x[0-9a-f]{40}|org_[A-Za-z0-9]{10,64})$/;
37
-
38
34
  function fail(path: string, reason: string): never {
39
35
  throw new AgentFileError(`${path}: ${reason}`);
40
36
  }
@@ -74,9 +70,6 @@ function optionalMatch(
74
70
  return value;
75
71
  }
76
72
 
77
- const connectorsOf = (raw: unknown): string[] =>
78
- Array.isArray(raw) ? raw.filter((id): id is string => typeof id === 'string' && ID_RE.test(id)) : [];
79
-
80
73
  function nameOf(raw: unknown, path: string): string | null {
81
74
  if (raw === undefined || raw === null) return null;
82
75
  if (typeof raw !== 'string' || !AGENT_NAME_RE.test(raw)) fail(path, 'name is not a valid agent name');
@@ -97,21 +90,13 @@ export function parseAgentFile(raw: string, path: string): AgentFile {
97
90
  fail(path, 'id is not an 11-character id');
98
91
  const label = nameOf(name, path);
99
92
  const key = optionalMatch(parsed.key, KEY_RE, path, 'key is not an agent key');
100
- const owner = optionalMatch(
101
- parsed.owner,
102
- OWNER_RE,
103
- path,
104
- 'owner is neither an organization id nor a lowercase Ethereum address',
105
- );
106
93
  if (!Array.isArray(stations)) fail(path, 'stations is not a list');
107
94
  return {
108
95
  version: 1,
109
96
  id,
110
97
  name: label,
111
98
  key,
112
- owner,
113
99
  stations: stations.map((s, i) => stationOf(s, path, i)),
114
- connectors: connectorsOf(parsed.connectors),
115
100
  };
116
101
  }
117
102
 
@@ -155,26 +140,8 @@ export function migrateAgentLayout(dir = agentsDir()): 'moved' | 'kept' | 'none'
155
140
  return 'moved';
156
141
  }
157
142
 
158
- function assertUnique(agents: AgentFile[], paths: string[]): void {
159
- const ids = new Map<string, string>();
160
- const keys = new Map<string, string>();
161
- agents.forEach((agent, i) => {
162
- const path = paths[i] ?? '';
163
- const sameId = ids.get(agent.id);
164
- if (sameId !== undefined) fail(path, `id ${agent.id} is already used by ${sameId}`);
165
- ids.set(agent.id, path);
166
- if (agent.key === null) return;
167
- const sameKey = keys.get(agent.key);
168
- if (sameKey !== undefined) fail(path, `its key is already used by ${sameKey}`);
169
- keys.set(agent.key, path);
170
- });
171
- }
172
-
173
143
  export function loadFileAgents(dir = agentsDir()): LoadedAgent[] {
174
- const paths = listAgentFiles(dir);
175
- const agents = paths.map(readAgentFile);
176
- assertUnique(agents, paths);
177
- return agents.map((agent) => ({
144
+ return listAgentFiles(dir).map(readAgentFile).map((agent) => ({
178
145
  id: agent.id,
179
146
  name: agent.name ?? agent.id,
180
147
  key: agent.key,
@@ -25,11 +25,6 @@ export function registerKey(key: string, agentId: string): void {
25
25
  agentIdByKeyHash.set(digest(key), agentId);
26
26
  }
27
27
 
28
- export function unregisterAgentKey(agentId: string): void {
29
- for (const [hash, id] of agentIdByKeyHash)
30
- if (id === agentId) agentIdByKeyHash.delete(hash);
31
- }
32
-
33
28
  export function agentIdForKey(token: string): string | undefined {
34
29
  if (token === '') return undefined;
35
30
  return agentIdByKeyHash.get(digest(token));
@@ -6,7 +6,8 @@ import { errMsg, log, logFatalSync } from '@metro-labs/core/log';
6
6
  import { acquireLock, STATE_DIR, trainsDir } from './paths.js';
7
7
  import { installCrashGuard, markDaemonReady } from './crash-guard.js';
8
8
  import { METRO_VERSION } from '@metro-labs/core/version';
9
- import { funnelDriver, Tunnel, tunnelWanted, webhookPort } from '../net/tunnel.js';
9
+ import { funnelDriver, Tunnel, tunnelWanted } from '../net/tunnel.js';
10
+ import { webhookPort } from '../files/attach-serve.js';
10
11
  import {
11
12
  localConnectHint,
12
13
  publicConnectHint,
@@ -31,10 +32,7 @@ import { migrateAgentLayout } from '../agents/files.js';
31
32
  import { ensureStationDeps } from '../stations/runtime-deps.js';
32
33
  import { localSessionApis } from '../routes/local-mode.js';
33
34
  import type { SessionApis } from '../routes/session-apis.js';
34
- import {
35
- agentLiveness,
36
- createMetroMcp,
37
- } from '../mcp/index.js';
35
+ import { createMetroMcp } from '../mcp/index.js';
38
36
  import { gatherAccountsForAgents } from '../mcp/accounts.js';
39
37
  import {
40
38
  accountStationCapabilities,
@@ -106,7 +104,6 @@ function sessionApis(): SessionApis {
106
104
  },
107
105
  gatherAccounts: gatherAccountsForAgents,
108
106
  capabilities: accountStationCapabilities,
109
- liveness: agentLiveness,
110
107
  prepareAccount,
111
108
  });
112
109
  }
@@ -117,7 +114,7 @@ async function main(): Promise<void> {
117
114
  installBearerSessions(agentsDir(), localOwner);
118
115
  migrateAgentLayout();
119
116
  log.info({ agent: await ensureLocalAgent() }, 'local daemon: agent');
120
- await materializeFrom(fileSource, { allowEmpty: true });
117
+ await materializeFrom(fileSource);
121
118
  supervisor.start();
122
119
  const metroMcp = await createMetroMcp();
123
120
  webhookServer = await startWebhookServer(
@@ -1,24 +1,12 @@
1
- import { accountFromLine, agentIdForAccount } from '../agents/map.js';
2
-
3
1
  const accountStrippedLine = (line: string): string => {
4
2
  const parts = line.split('/');
5
3
  if (parts.length < 5) return line;
6
4
  return [parts[0], parts[1], parts[2], ...parts.slice(4)].join('/');
7
5
  };
8
6
 
9
- export const dedupeOwner = (line: string): string => {
10
- const acct = accountFromLine(line);
11
- if (!acct) return 'unowned';
12
- const agentId = agentIdForAccount(acct.station, acct.accountId);
13
- return agentId === undefined
14
- ? `account:${acct.accountId}`
15
- : `agent:${agentId}`;
16
- };
17
-
18
7
  export const dedupeKey = (
19
8
  station: string,
20
9
  line: string,
21
10
  kind: string,
22
11
  messageId: string,
23
- ): string =>
24
- `${dedupeOwner(line)} ${station} ${accountStrippedLine(line)} ${kind} ${messageId}`;
12
+ ): string => `${station} ${accountStrippedLine(line)} ${kind} ${messageId}`;
@@ -83,7 +83,7 @@ export class ChannelRelay {
83
83
  private withhold(event: MetroEvent, busSeq: number): void {
84
84
  this.ledger.delivered.delete(busSeq);
85
85
  this.deps.log(
86
- 'relay: withheld (line outside the channel session scope)',
86
+ 'relay: withheld (no stream, or line outside the agent scope)',
87
87
  'busSeq',
88
88
  busSeq,
89
89
  'line',
@@ -43,7 +43,7 @@ import {
43
43
  type SessionDeps,
44
44
  } from './session.js';
45
45
  import { claudeVersion, updateClaude, type VersionDeps } from './version.js';
46
- import { readSessionFile, receiveSessionFile, SESSION_FILE_MAX, sessionFilePath, writeSessionFile } from './session-files.js';
46
+ import { receiveSessionFile, sessionFilePath } from './session-files.js';
47
47
  import { createReadStream } from 'node:fs';
48
48
  import { pipeline } from 'node:stream/promises';
49
49
  import {
@@ -60,13 +60,11 @@ import {
60
60
 
61
61
  const PREFIX = '/api/claude';
62
62
  const BODY_MAX = SETTINGS_MAX + 4096;
63
- const SESSION_BODY_MAX = SESSION_FILE_MAX + 4096;
64
63
  const WRITABLE = new Set(['GET', 'DELETE', 'PUT', 'POST']);
65
64
  const PAGE = 100;
66
65
  const PAGE_MAX = 500;
67
66
 
68
67
  export interface ClaudeApiDeps {
69
- authorize: (subject: string) => void;
70
68
  dir?: () => string;
71
69
  login?: LoginDeps;
72
70
  session?: SessionDeps;
@@ -98,7 +96,6 @@ const COLLECTIONS: Record<string, Handler> = {
98
96
 
99
97
  const ITEMS: Record<string, Handler> = {
100
98
  sessions: (query, dir, id) => {
101
- if (query.get('raw') === '1') return { id, text: readSessionFile(projectOf(query), id, dir) };
102
99
  const { offset, limit } = pageOf(query);
103
100
  return readTranscript(projectOf(query), id, offset, limit, dir);
104
101
  },
@@ -111,7 +108,7 @@ const parts = (path: string): string[] => path.slice(PREFIX.length + 1).split('/
111
108
  const seenIn = (body: Record<string, unknown>): string | null | undefined =>
112
109
  'seenAt' in body ? (typeof body.seenAt === 'string' ? body.seenAt : null) : undefined;
113
110
 
114
- const WRITE_HEADS = new Set(['settings', 'skills', 'memory', 'sessions']);
111
+ const WRITE_HEADS = new Set(['settings', 'skills', 'memory']);
115
112
 
116
113
  interface Write {
117
114
  text: string;
@@ -121,7 +118,6 @@ interface Write {
121
118
 
122
119
  function writeItem(head: string, item: string, write: Write, search: string, dir: string): unknown {
123
120
  if (head === 'skills') return writeClaudeSkill(decodeURIComponent(item), write.text, write.seen, dir);
124
- if (head === 'sessions') return writeSessionFile(projectOf(new URLSearchParams(search)), item, write.text, dir);
125
121
  if (head === 'memory') return writeMemoryFile(projectOf(new URLSearchParams(search)), decodeURIComponent(item), write.text, dir, write.modifiedAt);
126
122
  return writeClaudeSettings(item, write.text, write.seen, dir);
127
123
  }
@@ -129,7 +125,7 @@ function writeItem(head: string, item: string, write: Write, search: string, dir
129
125
  async function writeAnswer(req: IncomingMessage, path: string, search: string, dir: string): Promise<unknown> {
130
126
  const [head = '', item = ''] = parts(path);
131
127
  if (!WRITE_HEADS.has(head) || item === '') throw new ApiError('method not allowed', 405);
132
- const body = await readJsonBody(req, head === 'sessions' ? SESSION_BODY_MAX : BODY_MAX);
128
+ const body = await readJsonBody(req, BODY_MAX);
133
129
  if (!isRecord(body) || typeof body.text !== 'string') throw new ApiError('text is required', 400);
134
130
  const modifiedAt = typeof body.modifiedAt === 'string' ? body.modifiedAt : undefined;
135
131
  return writeItem(head, item, { text: body.text, seen: seenIn(body), modifiedAt }, search, dir);
@@ -315,7 +311,8 @@ async function streamed(req: IncomingMessage, res: ServerResponse, path: string,
315
311
  });
316
312
  return true;
317
313
  }
318
- if (req.method === 'PUT' && !(req.headers['content-type'] ?? '').includes('json')) {
314
+ if (req.method === 'PUT') {
315
+ if ((req.headers['content-type'] ?? '').includes('json')) throw new ApiError('send the session file as text/plain', 415);
319
316
  sendJson(req, res, 200, await receiveSessionFile(projectOf(query), item, req, dir));
320
317
  return true;
321
318
  }
@@ -340,7 +337,6 @@ export function handleClaudeRequest(
340
337
  apiSession(req)
341
338
  .then(async (session) => {
342
339
  if (!session) throw new ApiError('unauthorized', 401);
343
- deps.authorize(session.subject);
344
340
  if (req.method !== 'GET' && ADMIN_ONLY.test(path)) requireAdmin(session);
345
341
  if (await streamed(req, res, path, search, (deps.dir ?? claudeDir)())) return;
346
342
  sendJson(req, res, 200, await routed(req, path, search, deps));
@@ -2,21 +2,19 @@ import {
2
2
  closeSync,
3
3
  createReadStream,
4
4
  existsSync,
5
- mkdirSync,
6
5
  openSync,
7
6
  readdirSync,
8
7
  readSync,
9
- renameSync,
10
8
  rmSync,
11
9
  statSync,
12
10
  utimesSync,
13
- writeFileSync,
14
11
  } from 'node:fs';
15
12
  import { homedir } from 'node:os';
16
13
  import { join } from 'node:path';
17
14
  import { createInterface } from 'node:readline';
18
15
  import { ApiError } from '@metro-labs/http/api-error';
19
16
  import { isRecord } from '@metro-labs/core/is-record';
17
+ import { writeAtomic } from '@metro-labs/core/secure-fs';
20
18
 
21
19
  export const PROJECT_RE = /^[A-Za-z0-9._-]{1,200}$/;
22
20
  export const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/;
@@ -340,11 +338,8 @@ export function writeMemoryFile(project: string, name: string, text: string, dir
340
338
  throw new ApiError(`a memory file is at most ${String(MEMORY_MAX)} bytes`, 400);
341
339
  const folder = memoryDir(project, dir);
342
340
  const file = safeName(name, MEMORY_RE, 'memory file name');
343
- mkdirSync(folder, { recursive: true });
344
341
  const path = join(folder, file);
345
- const tmp = `${path}.metro-${String(process.pid)}`;
346
- writeFileSync(tmp, text, { mode: MEMORY_MODE });
347
- renameSync(tmp, path);
342
+ writeAtomic(path, text, MEMORY_MODE);
348
343
  const stamp = stampOf(modifiedAt);
349
344
  if (stamp !== null) utimesSync(path, stamp, stamp);
350
345
  const stat = statSync(path);
@@ -1,4 +1,4 @@
1
- import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
1
+ import { createWriteStream, existsSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs';
2
2
  import type { Readable } from 'node:stream';
3
3
  import { join } from 'node:path';
4
4
  import { ApiError } from '@metro-labs/http/api-error';
@@ -23,10 +23,6 @@ export function sessionFilePath(project: string, session: string, dir = claudeDi
23
23
  return path;
24
24
  }
25
25
 
26
- export function readSessionFile(project: string, session: string, dir = claudeDir()): string {
27
- return readFileSync(sessionFilePath(project, session, dir), 'utf8');
28
- }
29
-
30
26
  const HEAD_BYTES = 64 * 1024;
31
27
 
32
28
  async function spool(body: Readable, tmp: string): Promise<string> {
@@ -77,14 +73,3 @@ function firstLineIsJson(text: string): boolean {
77
73
  return false;
78
74
  }
79
75
  }
80
-
81
- export function writeSessionFile(project: string, session: string, text: string, dir = claudeDir()): SessionFile {
82
- if (!firstLineIsJson(text)) throw new ApiError('a session file is JSON lines, one Claude Code entry per line', 400);
83
- if (Buffer.byteLength(text, 'utf8') > SESSION_FILE_MAX) throw new ApiError(`a session file is at most ${String(SESSION_FILE_MAX)} bytes`, 400);
84
- const path = transcriptPath(project, session, dir);
85
- mkdirSync(join(path, '..'), { recursive: true });
86
- const tmp = `${path}.metro-${String(process.pid)}`;
87
- writeFileSync(tmp, text, { mode: FILE_MODE });
88
- renameSync(tmp, path);
89
- return { id: safeName(session, SESSION_RE, 'session id'), bytes: statSync(path).size };
90
- }
@@ -1,14 +1,14 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
3
  import { ApiError } from '@metro-labs/http/api-error';
4
4
  import { claudeDir, listClaudeProjects } from './files.js';
5
5
  import { isRecord } from '@metro-labs/core/is-record';
6
6
  import { errMsg } from '@metro-labs/core/log';
7
+ import { writeAtomic } from '@metro-labs/core/secure-fs';
7
8
 
8
9
  export const SETTINGS_MAX = 256 * 1024;
9
10
  const USER_ID = 'user';
10
11
  const LOCAL_SUFFIX = '.local';
11
- const DEFAULT_MODE = 0o644;
12
12
 
13
13
  export type SettingsScope = 'user' | 'project' | 'local';
14
14
 
@@ -92,23 +92,6 @@ function assertSettingsJson(text: string): void {
92
92
  if (!isRecord(parsed)) throw new ApiError('Claude Code settings must be a JSON object', 400);
93
93
  }
94
94
 
95
- function modeOf(path: string): number {
96
- try {
97
- return statSync(path).mode & 0o777;
98
- } catch {
99
- return DEFAULT_MODE;
100
- }
101
- }
102
-
103
- function writeAtomic(path: string, text: string): void {
104
- const mode = existsSync(path) ? modeOf(path) : DEFAULT_MODE;
105
- mkdirSync(dirname(path), { recursive: true });
106
- const tmp = `${path}.metro-${String(process.pid)}`;
107
- writeFileSync(tmp, text, { mode });
108
- chmodSync(tmp, mode);
109
- renameSync(tmp, path);
110
- }
111
-
112
95
  export function writeClaudeSettings(
113
96
  id: string,
114
97
  text: string,