@stage-labs/metro 0.1.0-beta.325 → 0.1.0-beta.327

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 (194) hide show
  1. package/dist/agent.js +1 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/lock.js +15 -3
  4. package/dist/root-release-io.js +1 -2
  5. package/dist/root-release.js +2 -3
  6. package/dist/route.js +1 -1
  7. package/dist/service.js +4 -4
  8. package/dist/version.js +1 -1
  9. package/package.json +2 -2
  10. package/runtime/node_modules/@metro-labs/core/package.json +3 -1
  11. package/runtime/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
  12. package/runtime/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
  13. package/runtime/node_modules/@metro-labs/core/src/brands.ts +100 -0
  14. package/runtime/node_modules/@metro-labs/core/src/favicon.ts +38 -0
  15. package/runtime/node_modules/@metro-labs/core/src/lines.ts +2 -8
  16. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +0 -6
  17. package/runtime/node_modules/@metro-labs/core/src/skill-source.ts +6 -6
  18. package/runtime/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
  19. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
  20. package/runtime/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
  21. package/runtime/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
  22. package/runtime/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
  23. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
  24. package/runtime/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
  25. package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
  26. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
  27. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +4 -4
  28. package/runtime/node_modules/@metro-labs/daemon/package.json +1 -1
  29. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +2 -6
  30. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +6 -6
  31. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +0 -24
  32. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -3
  33. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -4
  34. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +4 -9
  35. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -6
  36. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +3 -3
  37. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +2 -2
  38. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +1 -1
  39. package/runtime/node_modules/@metro-labs/daemon/src/claude/github-fetch.ts +2 -2
  40. package/runtime/node_modules/@metro-labs/daemon/src/claude/github-skills.ts +2 -2
  41. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +1 -1
  42. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +3 -2
  43. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  44. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +3 -7
  45. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +10 -19
  46. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +1 -5
  47. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +3 -4
  48. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +1 -2
  49. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +2 -4
  50. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  51. package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +2 -5
  52. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +1 -5
  53. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +2 -4
  54. package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/announced.ts +1 -1
  55. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +0 -1
  56. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +2 -0
  57. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/tailscale-guard.ts +26 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +1 -3
  59. package/runtime/node_modules/@metro-labs/daemon/src/routes/gmail-attach.ts +3 -3
  60. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +6 -21
  61. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -5
  62. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +6 -6
  63. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +0 -1
  64. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +12 -61
  65. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +13 -25
  66. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +5 -5
  67. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +2 -7
  68. package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +2 -6
  69. package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +12 -12
  70. package/runtime/node_modules/@metro-labs/discord-bot/package.json +1 -1
  71. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +5 -23
  72. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +1 -7
  73. package/runtime/node_modules/@metro-labs/discord-bot/src/channels.ts +2 -2
  74. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +10 -35
  75. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +5 -11
  76. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +6 -34
  77. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +11 -14
  78. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +1 -3
  79. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +2 -3
  80. package/runtime/node_modules/@metro-labs/gmail/package.json +1 -1
  81. package/runtime/node_modules/@metro-labs/gmail/src/accounts.ts +3 -3
  82. package/runtime/node_modules/@metro-labs/gmail/src/auth.ts +8 -8
  83. package/runtime/node_modules/@metro-labs/gmail/src/managed.ts +3 -2
  84. package/runtime/node_modules/@metro-labs/http/package.json +1 -1
  85. package/runtime/node_modules/@metro-labs/http/src/agent-signature.ts +1 -1
  86. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +3 -4
  87. package/runtime/node_modules/@metro-labs/http/src/connector-discovery.ts +0 -113
  88. package/runtime/node_modules/@metro-labs/http/src/connector-input.ts +1 -1
  89. package/runtime/node_modules/@metro-labs/http/src/connector-mcp.ts +12 -13
  90. package/runtime/node_modules/@metro-labs/http/src/connector-oauth.ts +2 -1
  91. package/runtime/node_modules/@metro-labs/http/src/connector-policy.ts +1 -6
  92. package/runtime/node_modules/@metro-labs/http/src/connector-public-url.ts +4 -2
  93. package/runtime/node_modules/@metro-labs/http/src/connector-tools.ts +43 -13
  94. package/runtime/node_modules/@metro-labs/http/src/connector-url.ts +0 -6
  95. package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +1 -1
  96. package/runtime/node_modules/@metro-labs/http/src/mcp-server-tools.ts +0 -2
  97. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +1 -1
  98. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +2 -2
  99. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +6 -7
  100. package/runtime/node_modules/@metro-labs/inference/package.json +2 -1
  101. package/runtime/node_modules/@metro-labs/inference/src/attempts.ts +3 -3
  102. package/runtime/node_modules/@metro-labs/inference/src/bedrock.ts +1 -1
  103. package/runtime/node_modules/@metro-labs/inference/src/codex-translate.ts +2 -2
  104. package/runtime/node_modules/@metro-labs/inference/src/delivered.ts +1 -1
  105. package/runtime/node_modules/@metro-labs/inference/src/effort.ts +0 -6
  106. package/runtime/node_modules/@metro-labs/inference/src/fallback.ts +1 -1
  107. package/runtime/node_modules/@metro-labs/inference/src/forward.ts +5 -5
  108. package/runtime/node_modules/@metro-labs/inference/src/measure.ts +2 -3
  109. package/runtime/node_modules/@metro-labs/inference/src/named.ts +3 -3
  110. package/runtime/node_modules/@metro-labs/inference/src/probes.ts +98 -0
  111. package/runtime/node_modules/@metro-labs/inference/src/usage.ts +5 -5
  112. package/runtime/node_modules/@metro-labs/outlook/package.json +1 -1
  113. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +6 -8
  114. package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +2 -1
  115. package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +3 -5
  116. package/runtime/node_modules/@metro-labs/outlook/src/me.ts +6 -6
  117. package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +10 -13
  118. package/runtime/node_modules/@metro-labs/telegram/package.json +1 -1
  119. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +7 -26
  120. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +1 -1
  121. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +3 -6
  122. package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +3 -2
  123. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +1 -1
  124. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +6 -20
  125. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +1 -7
  126. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +0 -2
  127. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +1 -16
  128. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +1 -8
  129. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +2 -1
  130. package/runtime/node_modules/@metro-labs/threema/package.json +1 -1
  131. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +1 -8
  132. package/runtime/node_modules/@metro-labs/threema/src/chats.ts +4 -3
  133. package/runtime/node_modules/@metro-labs/threema/src/format.ts +15 -17
  134. package/runtime/node_modules/@metro-labs/threema/src/groups.ts +2 -4
  135. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +2 -1
  136. package/runtime/node_modules/@metro-labs/threema/src/messages.ts +2 -1
  137. package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +7 -7
  138. package/runtime/node_modules/@metro-labs/webhook/package.json +1 -1
  139. package/runtime/node_modules/@metro-labs/whatsapp/package.json +1 -1
  140. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +1 -3
  141. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +0 -2
  142. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +1 -1
  143. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +1 -1
  144. package/runtime/node_modules/@metro-labs/xmtp/package.json +1 -1
  145. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +7 -0
  146. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +4 -10
  147. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -5
  148. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +14 -26
  149. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +1 -7
  150. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +17 -58
  151. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +1 -6
  152. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +10 -31
  153. package/runtime/node_modules/@metro-labs/xmtp/src/frames.ts +12 -14
  154. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +2 -3
  155. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +3 -9
  156. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +8 -9
  157. package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +4 -2
  158. package/runtime/node_modules/@metro-labs/xmtp/src/network.ts +2 -3
  159. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +2 -1
  160. package/runtime/node_modules/@metro-labs/xmtp/src/read-attachments.ts +2 -2
  161. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +2 -1
  162. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +23 -46
  163. package/runtime/node_modules/@metro-labs/xmtp/src/wallet.ts +5 -4
  164. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +12 -15
  165. package/runtime/runtime.json +1 -1
  166. package/runtime/sdk-runner/node_modules/@metro-labs/core/package.json +3 -1
  167. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
  168. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
  169. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/brands.ts +100 -0
  170. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/favicon.ts +38 -0
  171. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/lines.ts +2 -8
  172. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/protocol.ts +0 -6
  173. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/skill-source.ts +6 -6
  174. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
  175. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
  176. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
  177. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
  178. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
  179. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
  180. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
  181. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
  182. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
  183. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/train-error.ts +4 -4
  184. package/runtime/sdk-runner/runner.json +1 -1
  185. package/runtime/sdk-runner/src/approvals.ts +2 -2
  186. package/runtime/sdk-runner/src/automation-command.ts +2 -2
  187. package/runtime/sdk-runner/src/calls.ts +0 -6
  188. package/runtime/sdk-runner/src/errors.ts +1 -1
  189. package/runtime/sdk-runner/src/link.ts +1 -1
  190. package/runtime/sdk-runner/src/runner.ts +2 -2
  191. package/runtime/sdk-runner/src/session-watch.ts +5 -34
  192. package/runtime/sdk-runner/src/task-state.ts +1 -1
  193. package/runtime/sdk-runner/src/tool-proxy.ts +1 -1
  194. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +0 -40
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { isRecord } from '@metro-labs/core/is-record';
4
+ import { stringOf } from '@metro-labs/http/api-http';
4
5
  import type { AgentUser } from './user.js';
5
6
  import { helperRun, type RunResult } from './unit-files.js';
6
7
 
@@ -64,22 +65,21 @@ function listTimers(runner: Runner): Record<string, unknown>[] {
64
65
  }
65
66
  }
66
67
 
67
- const text = (v: unknown): string => (typeof v === 'string' ? v : '');
68
68
  const orRoot = (user: string | undefined): string => (user === undefined || user === '' ? 'root' : user);
69
69
 
70
70
  function timerJob(runner: Runner, t: Record<string, unknown>): ScheduledJob | null {
71
- const unit = text(t.unit);
72
- const service = text(t.activates);
71
+ const unit = stringOf(t.unit);
72
+ const service = stringOf(t.activates);
73
73
  if (unit === '' || service === '' || OWN_UNITS.test(service)) return null;
74
74
  const timer = showProps(runner, unit, ['FragmentPath', 'TimersCalendar', 'TimersMonotonic']);
75
- if (!text(timer.FragmentPath).startsWith(`${UNIT_DIR}/`)) return null;
75
+ if (!stringOf(timer.FragmentPath).startsWith(`${UNIT_DIR}/`)) return null;
76
76
  const svc = showProps(runner, service, ['ExecStart', 'User', 'Result']);
77
- const command = execArgv(text(svc.ExecStart)).join(' ');
77
+ const command = execArgv(stringOf(svc.ExecStart)).join(' ');
78
78
  return {
79
79
  id: `timer:${unit}`,
80
80
  kind: 'timer',
81
81
  name: unit.replace(/\.timer$/, ''),
82
- schedule: calendarOf(`${text(timer.TimersCalendar)} ${text(timer.TimersMonotonic)}`),
82
+ schedule: calendarOf(`${stringOf(timer.TimersCalendar)} ${stringOf(timer.TimersMonotonic)}`),
83
83
  command,
84
84
  runsAs: orRoot(svc.User),
85
85
  next: stamp(t.next),
@@ -1,6 +1,4 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import { homedir } from 'node:os';
4
2
  import { join } from 'node:path';
5
3
  import { runningAsMetro } from '../metro-user/privilege.js';
6
4
 
@@ -74,28 +72,6 @@ export const agentCommand = (command: readonly string[], extra: Record<string, s
74
72
  return [bin, ...argv];
75
73
  };
76
74
 
77
- const SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk';
78
-
79
- export const CLAUDE_ENV: Readonly<Record<string, string>> = { DISABLE_AUTOUPDATER: '1' };
80
-
81
- let musl: boolean | null = null;
82
-
83
- function muslFirst(): boolean {
84
- if (process.platform !== 'linux') return false;
85
- if (musl === null) {
86
- const report = process.report.getReport() as { header?: { glibcVersionRuntime?: string } };
87
- musl = report.header?.glibcVersionRuntime === undefined;
88
- }
89
- return musl;
90
- }
91
-
92
- export function claudeBin(user = agentUser()): string {
93
- const name = `${SDK_PACKAGE}-${process.platform}-${process.arch}`;
94
- const packages = muslFirst() ? [`${name}-musl`, name] : [name, `${name}-musl`];
95
- const [first = '', other = ''] = packages.map((pkg) => join(user?.home ?? homedir(), '.metro', 'sdk-runner', 'node_modules', pkg, 'claude'));
96
- return existsSync(first) || !existsSync(other) ? first : other;
97
- }
98
-
99
75
  export const claudeHome = (user = agentUser()): string | null => (user === null ? null : user.home);
100
76
 
101
77
  export const agentViewDir = (user: AgentUser): string => join(user.home, '.metro', 'agents');
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { errMsg, log } from '@metro-labs/core/log';
4
4
  import { isRecord } from '@metro-labs/core/is-record';
@@ -45,7 +45,7 @@ export function viewFiles(dir = agentsDir(), model: () => string | null = () =>
45
45
  ['agent.json', agentFile(dir)],
46
46
  ['model.json', model()],
47
47
  ]);
48
- for (const name of COPIED) files.set(name, existsSync(join(dir, name)) ? readText(join(dir, name)) : null);
48
+ for (const name of COPIED) files.set(name, readText(join(dir, name)));
49
49
  for (const name of NEVER_COPIED) files.set(name, null);
50
50
  return files;
51
51
  }
@@ -58,7 +58,7 @@ export function syncAgentView(user: AgentUser | null = agentUser(), dir = agents
58
58
  let changed = 0;
59
59
  for (const [name, text] of viewFiles(dir)) {
60
60
  const path = join(target, name);
61
- if (written.has(path) && written.get(path) === text) continue;
61
+ if (written.get(path) === text) continue;
62
62
  if (text === null) removeHome(path, false, user);
63
63
  else writeHomeText(path, text, 0o600, undefined, user);
64
64
  written.set(path, text);
@@ -6,13 +6,12 @@ import { ApiError } from '@metro-labs/http/api-error';
6
6
  import { isRecord } from '@metro-labs/core/is-record';
7
7
  import { parseId } from '@metro-labs/core/ids';
8
8
  import type { LoadedAccount, LoadedAgent } from '../stations/materialize.js';
9
- import { STATIONS } from '@metro-labs/core/station-names';
10
9
  import { approversOf } from './files.js';
10
+ import { isStationName } from './account-attach.js';
11
11
 
12
12
  const AGENTS = '/api/agents';
13
13
  const RESTORE_PATH = `${AGENTS}/restore`;
14
14
  const BUNDLE_MAX = 2 * 1024 * 1024;
15
- const STATION_NAMES = new Set<string>(STATIONS);
16
15
 
17
16
  export interface AgentBundle {
18
17
  version: 1;
@@ -50,7 +49,7 @@ function parseMode(raw: unknown): ImportMode {
50
49
  const bad = (what: string): ApiError => new ApiError(`bundle: ${what}`, 400);
51
50
 
52
51
  function stationOf(raw: unknown): LoadedAccount {
53
- if (!isRecord(raw) || typeof raw.station !== 'string' || !STATION_NAMES.has(raw.station))
52
+ if (!isRecord(raw) || !isStationName(raw.station))
54
53
  throw bad('a station is not a known station');
55
54
  if (typeof raw.id !== 'string' || !isRecord(raw.config)) throw bad('a station has no id or config');
56
55
  const allowlist = Array.isArray(raw.allowlist)
@@ -58,7 +57,7 @@ function stationOf(raw: unknown): LoadedAccount {
58
57
  : null;
59
58
  const policy = parsePolicy(raw.policy, `bundle ${raw.station}/${raw.id}`);
60
59
  return {
61
- station: raw.station as LoadedAccount['station'],
60
+ station: raw.station,
62
61
  id: raw.id,
63
62
  allowlist,
64
63
  ...approversOf(raw.approvers),
@@ -33,8 +33,6 @@ interface Stored {
33
33
  file: AgentFile;
34
34
  }
35
35
 
36
- const missing = (): AgentAdminError => new AgentAdminError('no such agent', 404);
37
-
38
36
  function parseOwner(raw: string): string | null {
39
37
  const text = raw.trim();
40
38
  return isOrganizationId(text) ? text : null;
@@ -66,7 +64,7 @@ function save(stored: Stored): void {
66
64
 
67
65
  function agentOrThrow(id: string, dir: string): Stored {
68
66
  const found = storedAgents(dir).find((s) => s.file.id === id);
69
- if (found === undefined) throw missing();
67
+ if (found === undefined) throw new AgentAdminError('no such agent', 404);
70
68
  return found;
71
69
  }
72
70
 
@@ -84,9 +82,8 @@ function freshId(taken: Set<string>): string {
84
82
 
85
83
  export async function localCreateAgent(rawName?: string, dir = agentsDir()): Promise<CreatedAgent> {
86
84
  const name = rawName === undefined ? null : normalizeAgentName(rawName);
87
- const existing = storedAgents(dir);
88
- if (existing.length > 0) throw new AgentAdminError('this box already has its agent', 409);
89
- const id = freshId(new Set());
85
+ if (storedAgents(dir).length > 0) throw new AgentAdminError('this box already has its agent', 409);
86
+ const id = newId();
90
87
  const key = newApiKey();
91
88
  ensureSecureDir(dir);
92
89
  save({
@@ -228,9 +225,7 @@ export function localPairAgain(agentId: string, station: StationName, accountId:
228
225
  }
229
226
 
230
227
  export function readLocalAgentFile(agentId: string, dir = agentsDir()): AgentFile {
231
- const found = storedAgents(dir).find((s) => s.file.id === agentId);
232
- if (found === undefined) throw missing();
233
- return found.file;
228
+ return agentOrThrow(agentId, dir).file;
234
229
  }
235
230
 
236
231
  const UNIQUE_CREDENTIALS: Record<string, string> = {
@@ -2,15 +2,14 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { AGENT_NAME_RE, ID_RE } from '@metro-labs/core/ids';
5
- import { STATIONS, type StationName } from '@metro-labs/core/station-names';
6
5
  import type { LoadedAccount, LoadedAgent, StationSource } from '../stations/materialize.js';
7
6
  import { isRecord } from '@metro-labs/core/is-record';
8
7
  import { parsePolicy } from '../policy/policy.js';
8
+ import { isStationName } from './account-attach.js';
9
9
 
10
10
  const AGENT_FILE = 'agent.json';
11
11
 
12
12
  const KEY_RE = /^[A-Za-z0-9_-]{16,128}$/;
13
- const STATION_NAMES = new Set<string>(STATIONS);
14
13
 
15
14
  export class AgentFileError extends Error {}
16
15
 
@@ -35,10 +34,6 @@ function fail(path: string, reason: string): never {
35
34
  throw new AgentFileError(`${path}: ${reason}`);
36
35
  }
37
36
 
38
- function isStationName(value: unknown): value is StationName {
39
- return typeof value === 'string' && STATION_NAMES.has(value);
40
- }
41
-
42
37
  function allowlistOf(raw: unknown, path: string, where: string): string[] | null {
43
38
  if (raw === null || raw === undefined) return null;
44
39
  if (!Array.isArray(raw)) fail(path, `${where}.allowlist is not a list of strings`);
@@ -11,7 +11,7 @@ import { isMetroNotice } from './notice.js';
11
11
  import { replyMeta } from './addressed.js';
12
12
  import { reactContent, reactionEmoji } from './react-note.js';
13
13
  import {
14
- capSet,
14
+ capSize,
15
15
  displayNameMeta,
16
16
  missingAttachmentNames,
17
17
  senderMeta,
@@ -76,7 +76,7 @@ export class InboundRelay {
76
76
  noteSent(messageId: string): void {
77
77
  if (!messageId) return;
78
78
  this.sentIds.add(messageId);
79
- capSet(this.sentIds, SENT_IDS_MAX);
79
+ capSize(this.sentIds, SENT_IDS_MAX);
80
80
  }
81
81
 
82
82
  private notify(method: string, params: Record<string, unknown>): Promise<void> {
@@ -283,7 +283,7 @@ export class InboundRelay {
283
283
  }
284
284
  if (!base.line) return;
285
285
  this.allowedLines.add(base.line);
286
- capSet(this.allowedLines, ALLOWED_LINES_MAX);
286
+ capSize(this.allowedLines, ALLOWED_LINES_MAX);
287
287
  }
288
288
 
289
289
  private droppedSender(ev: Record<string, unknown>, from: string, line: string): boolean {
@@ -36,9 +36,9 @@ export interface MediaCtx {
36
36
  callMeta?: () => Record<string, string>;
37
37
  }
38
38
 
39
- export function capSet(set: Set<string>, max: number): void {
39
+ export function capSize(set: Set<string> | Map<string, unknown>, max: number): void {
40
40
  while (set.size > max) {
41
- const oldest = set.values().next();
41
+ const oldest = set.keys().next();
42
42
  if (oldest.done) break;
43
43
  set.delete(oldest.value);
44
44
  }
@@ -152,7 +152,7 @@ function sessionOf(path: string, id: string, mtime: number, size: number): Claud
152
152
  return {
153
153
  id,
154
154
  title: (titled === undefined ? null : str(titled.aiTitle)) ?? firstPrompt(head) ?? 'Untitled session',
155
- startedAt: str(head.find((l) => str(l.timestamp) !== null)?.timestamp) ?? null,
155
+ startedAt: str(head.find((l) => str(l.timestamp) !== null)?.timestamp),
156
156
  lastAt: new Date(mtime).toISOString(),
157
157
  bytes: size,
158
158
  gitBranch: str(meta.gitBranch),
@@ -142,7 +142,7 @@ function immutableCommit(data: unknown): { sha: string; tree: string } {
142
142
 
143
143
  function regularFile(entry: Entry): void {
144
144
  if (entry.type !== 'blob' || !['100644', '100755'].includes(entry.mode)) throw new GitHubFailure('Symlinks, submodules and special files are not supported in skills.');
145
- if (!skillPath(entry.path) || entry.size < 0 || entry.size > SKILL_FILE_MAX || !Number.isInteger(entry.size)) throw new GitHubFailure('A skills path or file size is not supported. Use skill-name/SKILL.md and regular resources, up to 256 KiB each.');
145
+ if (!skillPath(entry.path) || entry.size < 0 || entry.size > SKILL_FILE_MAX || !Number.isInteger(entry.size)) throw new GitHubFailure(`A skills path or file size is not supported. Use skill-name/SKILL.md and regular resources, up to ${SKILL_FILE_MAX / 1024} KiB each.`);
146
146
  }
147
147
 
148
148
  function checkedEntries(entries: Entry[]): Entry[] {
@@ -154,7 +154,7 @@ function checkedEntries(entries: Entry[]): Entry[] {
154
154
  total += entry.size;
155
155
  files.push(entry);
156
156
  }
157
- if (files.length > SKILL_FILES_MAX || total > SKILL_TOTAL_MAX) throw new GitHubFailure('Skills exceed 256 files or 8 MiB.');
157
+ if (files.length > SKILL_FILES_MAX || total > SKILL_TOTAL_MAX) throw new GitHubFailure(`Skills exceed ${SKILL_FILES_MAX} files or ${SKILL_TOTAL_MAX / 1024 / 1024} MiB.`);
158
158
  if (new Set(files.map((entry) => entry.path.toLowerCase())).size !== files.length) throw new GitHubFailure('Skills contain duplicate file paths.');
159
159
  return files;
160
160
  }
@@ -4,7 +4,7 @@ import { Readable } from 'node:stream';
4
4
  import { log } from '@metro-labs/core/log';
5
5
  import { isRecord } from '@metro-labs/core/is-record';
6
6
  import { writeSecure } from '@metro-labs/core/secure-fs';
7
- import { SKILL_HASH, SKILL_MANIFEST_MAX, skillGeneration, skillRelease, skillSourceRoot, type SkillGeneration } from '@metro-labs/core/skill-source';
7
+ import { SKILL_FILE_MAX, SKILL_HASH, SKILL_MANIFEST_MAX, skillGeneration, skillRelease, skillSourceRoot, type SkillGeneration } from '@metro-labs/core/skill-source';
8
8
  import { ApiError } from '@metro-labs/http/api-error';
9
9
  import { readFileSync, statSync } from '../agent-user/agent-fs.js';
10
10
  import { receiveHomeFile, writeHomeText } from '../agent-user/home-fs.js';
@@ -135,7 +135,7 @@ export class GitHubSkills {
135
135
  read(id: string): ClaudeSkill & { text: string } {
136
136
  const row = this.rows().find((skill) => skill.id === id);
137
137
  if (row === undefined) throw new ApiError('no such skill', 404);
138
- if (statSync(row.path).size > 256 * 1024) throw new ApiError('The managed skill changed on disk. Sync it again.', 409);
138
+ if (statSync(row.path).size > SKILL_FILE_MAX) throw new ApiError('The managed skill changed on disk. Sync it again.', 409);
139
139
  return { ...row, text: readFileSync(row.path, 'utf8') };
140
140
  }
141
141
 
@@ -104,7 +104,7 @@ const skillTemplate = (name: string): string =>
104
104
  ['---', `name: ${name}`, 'description: what this skill does, and when Claude should reach for it', '---', '', `# ${name}`, '', 'Write the instructions here.', ''].join('\n');
105
105
 
106
106
  export function createClaudeSkill(name: string, text: string | undefined, dir = claudeDir()): ClaudeSkill {
107
- if (typeof name !== 'string' || !SKILL_NAME_RE.test(name))
107
+ if (!SKILL_NAME_RE.test(name))
108
108
  throw new ApiError('a skill name is lowercase letters, digits and dashes, up to 64 characters', 400);
109
109
  const path = join(userSkillsRoot(dir), name, FILE);
110
110
  if (existsSync(path)) throw new ApiError('a skill by that name already lives there', 409);
@@ -1,5 +1,6 @@
1
1
  import { randomBytes, timingSafeEqual } from 'node:crypto';
2
2
  import { ID_RE } from '@metro-labs/core/ids';
3
+ import { isRecord } from '@metro-labs/core/is-record';
3
4
  import { readFileSync } from 'node:fs';
4
5
  import { resolveCachedAttachment } from '@metro-labs/core/stations/attachments';
5
6
  import { writeSecure } from '@metro-labs/core/secure-fs';
@@ -23,8 +24,8 @@ function parseGrant(raw: string): AttachmentGrant | undefined {
23
24
  } catch {
24
25
  return undefined;
25
26
  }
26
- if (typeof parsed !== 'object' || parsed === null) return undefined;
27
- const { token, agentId, mintedAt } = parsed as Record<string, unknown>;
27
+ if (!isRecord(parsed)) return undefined;
28
+ const { token, agentId, mintedAt } = parsed;
28
29
  if (typeof token !== 'string' || token === '') return undefined;
29
30
  if (typeof agentId !== 'string' || !ID_RE.test(agentId)) return undefined;
30
31
  return {
@@ -40,7 +40,7 @@ export function attachmentUrl(
40
40
  pathOrName: string,
41
41
  agentId: string,
42
42
  ): string | null {
43
- const base = publicBaseUrl() ?? loopbackBase();
43
+ const base = publicBaseOrDefault();
44
44
  if (!base) return null;
45
45
  const name = pathOrName.split('/').pop();
46
46
  if (!name || !resolveCachedAttachment(name)) return null;
@@ -37,11 +37,8 @@ const VAGUE_MIMES = new Set([
37
37
 
38
38
  const mib = (n: number): string => `${Math.round(n / (1024 * 1024))} MiB`;
39
39
 
40
- const uploadLimit = (): string =>
41
- `${mib(MAX_UPLOAD_BYTES)} (${MAX_UPLOAD_BYTES} bytes)`;
42
-
43
40
  const tooLarge = (): string =>
44
- `upload exceeds the ${uploadLimit()} per-file limit; ` +
41
+ `upload exceeds the ${mib(MAX_UPLOAD_BYTES)} (${MAX_UPLOAD_BYTES} bytes) per-file limit; ` +
45
42
  'send a link in `text` for anything larger';
46
43
 
47
44
  const noRoom = (): string =>
@@ -96,9 +93,8 @@ function declaredLength(req: IncomingMessage): number | undefined {
96
93
 
97
94
  function assertRoomForDeclared(req: IncomingMessage, room: number): void {
98
95
  const declared = declaredLength(req);
99
- if (declared === undefined) return;
100
- if (declared > MAX_UPLOAD_BYTES) throw new ApiError(tooLarge(), 413);
101
- if (declared > room) throw new ApiError(noRoom(), 507);
96
+ const over = declared === undefined ? undefined : overflowAt(declared, room);
97
+ if (over !== undefined) throw over;
102
98
  }
103
99
 
104
100
  async function drainInto(
@@ -7,6 +7,7 @@ import {
7
7
  import { listEndpoints } from '../stations/webhook-endpoints.js';
8
8
  import { hookUrl, threemaCallbackUrl } from '../stations/attach.js';
9
9
  import { accountStatusOf } from '@metro-labs/core/stations/account-status';
10
+ import { isRecord } from '@metro-labs/core/is-record';
10
11
  import { accountEnabled, agentIdForAccount, allowlistForAccount, approversForAccount, knownAccounts, type KnownAccount } from '../agents/map.js';
11
12
 
12
13
  const accountId = (acc: unknown): string | undefined => {
@@ -14,16 +15,9 @@ const accountId = (acc: unknown): string | undefined => {
14
15
  return typeof id === 'string' ? id : undefined;
15
16
  };
16
17
 
17
- const asRecord = (acc: unknown): Record<string, unknown> | undefined =>
18
- typeof acc === 'object' && acc !== null && !Array.isArray(acc)
19
- ? (acc as Record<string, unknown>)
20
- : undefined;
21
-
22
18
  function withAgentId(station: string, acc: unknown): unknown {
23
- const rec = asRecord(acc);
24
- if (rec === undefined) return acc;
25
- const id = typeof rec.id === 'string' ? rec.id : undefined;
26
- if (id === undefined) return acc;
19
+ if (!isRecord(acc) || typeof acc.id !== 'string') return acc;
20
+ const id = acc.id;
27
21
  const agentId = agentIdForAccount(station, id);
28
22
  if (agentId === undefined) return acc;
29
23
  const allowlist = allowlistForAccount(station, id);
@@ -31,7 +25,7 @@ function withAgentId(station: string, acc: unknown): unknown {
31
25
  const policy = stored === undefined ? {} : { policy: stored };
32
26
  const approvers = approversForAccount(station, id);
33
27
  const extra = approvers.length === 0 ? policy : { ...policy, approvers };
34
- const tagged = allowlist === undefined ? { ...rec, agentId, ...extra } : { ...rec, agentId, allowlist, ...extra };
28
+ const tagged = allowlist === undefined ? { ...acc, agentId, ...extra } : { ...acc, agentId, allowlist, ...extra };
35
29
  return accountEnabled(station, id) ? tagged : { ...tagged, enabled: false };
36
30
  }
37
31
 
@@ -79,17 +73,15 @@ function inCoreAccounts(station: string): unknown[] {
79
73
  }
80
74
 
81
75
  function withCallbackUrl(acc: unknown): unknown {
82
- const rec = asRecord(acc);
83
- if (rec === undefined) return acc;
84
- const { callbackId, callbackToken, ...rest } = rec;
76
+ if (!isRecord(acc)) return acc;
77
+ const { callbackId, callbackToken, ...rest } = acc;
85
78
  if (typeof callbackId !== 'string' || typeof callbackToken !== 'string') return rest;
86
79
  return { ...rest, callback: threemaCallbackUrl(callbackId, callbackToken) };
87
80
  }
88
81
 
89
82
  function withStatus(reports: boolean, acc: unknown): unknown {
90
- const rec = asRecord(acc);
91
- if (rec === undefined || !('status' in rec)) return acc;
92
- const { status, ...rest } = rec;
83
+ if (!isRecord(acc) || !('status' in acc)) return acc;
84
+ const { status, ...rest } = acc;
93
85
  const live = reports ? accountStatusOf(status) : null;
94
86
  return live === null ? rest : { ...rest, status: live };
95
87
  }
@@ -168,9 +160,8 @@ async function loadStations(): Promise<ScopedAccounts> {
168
160
  const URL_SECRETS = new Set(['endpoint', 'callback']);
169
161
 
170
162
  function withoutUrlSecrets(acc: unknown): unknown {
171
- const rec = asRecord(acc);
172
- if (rec === undefined) return acc;
173
- return Object.fromEntries(Object.entries(rec).filter(([key]) => !URL_SECRETS.has(key)));
163
+ if (!isRecord(acc)) return acc;
164
+ return Object.fromEntries(Object.entries(acc).filter(([key]) => !URL_SECRETS.has(key)));
174
165
  }
175
166
 
176
167
  export async function gatherAccounts(
@@ -139,10 +139,6 @@ function refusal(m: MessageArgs, text: string | undefined, requested: CanonicalA
139
139
  return undefined;
140
140
  }
141
141
 
142
- function noteSent(m: MessageArgs, messageId: string | undefined): void {
143
- if (messageId !== undefined) m.onSent?.(messageId);
144
- }
145
-
146
142
  async function handleSend(m: MessageArgs): Promise<ToolResult> {
147
143
  if (m.a.speech !== undefined) return sendSpeech(m.line, m.a);
148
144
  const text = m.a.text as string | undefined;
@@ -161,7 +157,7 @@ async function handleSend(m: MessageArgs): Promise<ToolResult> {
161
157
  ? await sendNative(m, text, replyTo, atts)
162
158
  : await sendForwarded(m, text, replyTo, atts);
163
159
  if (!sent.labels.length) return errResult(NOTHING_TO_SEND);
164
- noteSent(m, sent.messageId);
160
+ if (sent.messageId !== undefined) m.onSent?.(sent.messageId);
165
161
  const done = withId(`sent: ${sent.labels.join(', ')}`, sent.messageId);
166
162
  return ok(sent.thread === undefined ? done : `${done} — new thread line: ${sent.thread}`);
167
163
  } finally {
@@ -5,6 +5,7 @@ import type {
5
5
  } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
6
  import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
7
7
  import { str } from '@metro-labs/core/str';
8
+ import { isRecord } from '@metro-labs/core/is-record';
8
9
  import { eventInScope } from '../agents/scope.js';
9
10
  import { CALL_NOTICE, CALL_STATE } from '@metro-labs/core/call';
10
11
 
@@ -28,10 +29,8 @@ const SEP = '_';
28
29
 
29
30
  function frameLine(message: JSONRPCMessage): string | undefined {
30
31
  const params: unknown = (message as { params?: unknown }).params;
31
- if (typeof params !== 'object' || params === null) return undefined;
32
- const meta: unknown = (params as { meta?: unknown }).meta;
33
- if (typeof meta !== 'object' || meta === null) return undefined;
34
- return str((meta as { line?: unknown }).line) || undefined;
32
+ if (!isRecord(params) || !isRecord(params.meta)) return undefined;
33
+ return str(params.meta.line) || undefined;
35
34
  }
36
35
 
37
36
  const isCallNotice = (message: JSONRPCMessage): boolean => 'method' in message && (message.method === CALL_NOTICE || message.method === CALL_STATE);
@@ -65,9 +65,8 @@ function forwardArgs(a: Record<string, unknown>): Record<string, unknown> {
65
65
  const args: Record<string, unknown> = {};
66
66
  const members = strList(a.members);
67
67
  if (members.length) args.members = members;
68
- for (const key of ['name', 'account', 'parent', 'addresses', 'inboxIds'])
68
+ for (const key of ['name', 'account', 'parent', 'addresses', 'inboxIds', 'private'])
69
69
  if (a[key] !== undefined) args[key] = a[key];
70
- if (a.private !== undefined) args.private = a.private;
71
70
  return args;
72
71
  }
73
72
 
@@ -15,6 +15,7 @@ import {
15
15
  serveChannelGet,
16
16
  } from './raw-get-stream.js';
17
17
  import { errMsg } from '@metro-labs/core/log';
18
+ import { isRecord } from '@metro-labs/core/is-record';
18
19
  import { channelLog, type McpSession } from './session.js';
19
20
  import { SessionSlot } from './session-slot.js';
20
21
  import { connectCallBridge } from './call-bridge.js';
@@ -22,10 +23,7 @@ import { connectServerApprovals } from './server-approvals.js';
22
23
  import { handleIncomingDiagnostic } from './diagnostics.js';
23
24
  import { onConnectorToolsChanged } from '../mcp-server/connector-tools.js';
24
25
 
25
- const isInitialize = (b: unknown): boolean =>
26
- !!b &&
27
- typeof b === 'object' &&
28
- (b as { method?: string }).method === 'initialize';
26
+ const isInitialize = (b: unknown): boolean => isRecord(b) && b.method === 'initialize';
29
27
 
30
28
  const MCP_BODY_MAX = 32 * 1024 * 1024;
31
29
 
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@metro-labs/core/is-record';
1
2
  import { str } from '@metro-labs/core/str';
2
3
  import type { ToolResult } from '@metro-labs/core/stations/types';
3
4
  import { accountFromLine, knownAccounts } from '../agents/map.js';
@@ -22,7 +23,7 @@ function channelTargets(name: string, a: Record<string, unknown>): ChannelTarget
22
23
  if (UNGATED.has(name)) return [];
23
24
  if (name === 'get_profile') {
24
25
  const line = profileScopeLine(a);
25
- return line === undefined ? [] : fromLine(line, undefined);
26
+ return line === undefined ? [] : fromLine(line);
26
27
  }
27
28
  const account = str(a.account);
28
29
  if (name === 'list_channels') return accountTargets(name, { account });
@@ -79,10 +80,8 @@ function effectivePolicy(target: ChannelTarget): EffectivePolicy {
79
80
  export function withEffectivePolicy(byStation: Record<string, unknown[]>): Record<string, unknown[]> {
80
81
  const out: Record<string, unknown[]> = {};
81
82
  for (const [station, rows] of Object.entries(byStation))
82
- out[station] = rows.map((row) => {
83
- if (typeof row !== 'object' || row === null || Array.isArray(row)) return row;
84
- const id = (row as { id?: unknown }).id;
85
- return typeof id === 'string' ? { ...row, policy: effectivePolicy({ kind: 'channel', station, account: id }) } : row;
86
- });
83
+ out[station] = rows.map((row) =>
84
+ isRecord(row) && typeof row.id === 'string' ? { ...row, policy: effectivePolicy({ kind: 'channel', station, account: row.id }) } : row,
85
+ );
87
86
  return out;
88
87
  }
@@ -4,6 +4,7 @@ import type { McpApproval, McpDecision } from '@metro-labs/http/mcp-server-proto
4
4
  import { holdPrompt, isPromptHeld, type Behavior } from '../approvals/pending.js';
5
5
  import { ownerLine } from '../approvals/owner-line.js';
6
6
  import { metroNotice } from '../channels/notice.js';
7
+ import { capSize } from '../channels/pending.js';
7
8
  import { announcedLine, noteAnnounced } from '../mcp-server/announced.js';
8
9
  import type { AnswerOutcome } from '../mcp-server/link.js';
9
10
  import { serverPromptBody } from './permission-prompt.js';
@@ -32,11 +33,7 @@ const noticeLineFor = (line: string | undefined): string | undefined => line ??
32
33
  function remember(id: string, now = Date.now()): void {
33
34
  for (const [seen, at] of recent) if (now - at >= RECENT_MS) recent.delete(seen);
34
35
  recent.set(id, now);
35
- while (recent.size > RECENT_MAX) {
36
- const oldest = recent.keys().next();
37
- if (oldest.done) break;
38
- recent.delete(oldest.value);
39
- }
36
+ capSize(recent, RECENT_MAX);
40
37
  }
41
38
 
42
39
  const answeredRecently = (id: string, now = Date.now()): boolean => now - (recent.get(id) ?? -Infinity) < RECENT_MS;
@@ -197,7 +197,7 @@ export class McpSession {
197
197
  if (sink !== undefined) {
198
198
  this.lastStreamAttachedAt = new Date().toISOString();
199
199
  this.announceCallState();
200
- this.announceToolSchema();
200
+ this.toolsChanged();
201
201
  }
202
202
  }
203
203
 
@@ -216,10 +216,6 @@ export class McpSession {
216
216
  }
217
217
 
218
218
  toolsChanged(): void {
219
- this.announceToolSchema();
220
- }
221
-
222
- private announceToolSchema(): void {
223
219
  if (!this.streamAttached) return;
224
220
  this.deliverSchemaNotice(
225
221
  () => this.server.sendToolListChanged(),
@@ -41,21 +41,19 @@ export const declaredGroup = (name: string): ToolGroup | undefined => BY_NAME.ge
41
41
 
42
42
  export const toolGroupOf = (name: string): ToolGroup => declaredGroup(name) ?? 'write';
43
43
 
44
- export type ToolAnnotations = Record<string, unknown>;
45
-
46
44
  export interface PublishedTool {
47
45
  name: string;
48
46
  title?: string;
49
47
  description: string;
50
48
  inputSchema: Record<string, unknown>;
51
- annotations: ToolAnnotations;
49
+ annotations: Record<string, unknown>;
52
50
  _meta?: Record<string, boolean>;
53
51
  }
54
52
 
55
53
  const ALWAYS_LOADED = new Set(['react', 'typing', 'send', 'reply']);
56
54
  const ALWAYS_LOAD = { 'anthropic/alwaysLoad': true };
57
55
 
58
- const annotationsOf = (def: ToolDef): ToolAnnotations =>
56
+ const annotationsOf = (def: ToolDef): Record<string, unknown> =>
59
57
  toolGroupOf(def.name) === 'read'
60
58
  ? { readOnlyHint: true }
61
59
  : { readOnlyHint: false, destructiveHint: def.destructive === true };
@@ -37,7 +37,7 @@ function prune(now: number): void {
37
37
 
38
38
  function save(): void {
39
39
  if (file === undefined) return;
40
- const announced = Object.fromEntries([...known].map(([id, entry]) => [id, { line: entry.line, at: entry.at }]));
40
+ const announced = Object.fromEntries(known);
41
41
  try {
42
42
  writeSecure(file, `${JSON.stringify({ version: 1, announced })}\n`);
43
43
  } catch (err) {
@@ -16,7 +16,6 @@ export const sudoersText = (agent: string): string =>
16
16
  [
17
17
  `${METRO_USER} ALL=(${agent}) NOPASSWD: ALL`,
18
18
  `${METRO_USER} ALL=(root) NOPASSWD: ${HELPER_PATH} *`,
19
- `Defaults:${METRO_USER} !requiretty`,
20
19
  '',
21
20
  ].join('\n');
22
21
 
@@ -14,6 +14,7 @@ import {
14
14
  upgradeUnitText,
15
15
  } from './helper-script.js';
16
16
  import { AGENT_NAME } from '../agent-user/user.js';
17
+ import { hardenTailscale } from './tailscale-guard.js';
17
18
 
18
19
  interface InstallPaths {
19
20
  helper: string;
@@ -72,6 +73,7 @@ if (import.meta.main) {
72
73
  installRootHelper();
73
74
  installImdsGuard();
74
75
  installUpgradeUnit(node);
76
+ for (const note of hardenTailscale()) process.stderr.write(`metro: ${note}\n`);
75
77
  process.stdout.write(
76
78
  `metro: wrote ${HELPER_PATH}, ${SUDOERS_PATH}, ${IMDS_UNIT_PATH} and ${UPGRADE_UNIT_PATH}; only root and metro reach the instance metadata service, and the root side follows every update\n`,
77
79
  );