@stage-labs/metro 0.1.0-beta.324 → 0.1.0-beta.326

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 (176) 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 +1 -1
  10. package/runtime/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
  11. package/runtime/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
  12. package/runtime/node_modules/@metro-labs/core/src/lines.ts +2 -8
  13. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +0 -6
  14. package/runtime/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
  15. package/runtime/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
  16. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
  17. package/runtime/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
  18. package/runtime/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
  19. package/runtime/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
  20. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
  21. package/runtime/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
  22. package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
  23. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
  24. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +4 -4
  25. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +2 -6
  26. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +6 -6
  27. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +0 -24
  28. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -3
  29. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -4
  30. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +4 -9
  31. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -6
  32. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +3 -3
  33. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +2 -2
  34. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +1 -1
  35. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +1 -1
  36. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +3 -2
  37. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  38. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +3 -7
  39. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +2 -2
  40. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +10 -19
  41. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +1 -5
  42. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +3 -4
  43. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +1 -2
  44. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +2 -4
  45. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  46. package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +2 -5
  47. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +1 -5
  48. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +2 -4
  49. package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/announced.ts +1 -1
  50. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +0 -1
  51. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +2 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/tailscale-guard.ts +26 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +1 -3
  54. package/runtime/node_modules/@metro-labs/daemon/src/routes/gmail-attach.ts +3 -3
  55. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +6 -21
  56. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -5
  57. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +6 -6
  58. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +0 -1
  59. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +12 -61
  60. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +13 -25
  61. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +5 -5
  62. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +2 -7
  63. package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +2 -6
  64. package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +12 -12
  65. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +5 -23
  66. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +1 -7
  67. package/runtime/node_modules/@metro-labs/discord-bot/src/channels.ts +2 -2
  68. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +10 -35
  69. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +5 -11
  70. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +6 -34
  71. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +11 -14
  72. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +1 -3
  73. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +2 -3
  74. package/runtime/node_modules/@metro-labs/gmail/src/accounts.ts +3 -3
  75. package/runtime/node_modules/@metro-labs/gmail/src/auth.ts +8 -8
  76. package/runtime/node_modules/@metro-labs/gmail/src/managed.ts +3 -2
  77. package/runtime/node_modules/@metro-labs/http/src/agent-signature.ts +1 -1
  78. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +3 -4
  79. package/runtime/node_modules/@metro-labs/http/src/connector-discovery.ts +0 -113
  80. package/runtime/node_modules/@metro-labs/http/src/connector-input.ts +1 -1
  81. package/runtime/node_modules/@metro-labs/http/src/connector-mcp.ts +12 -13
  82. package/runtime/node_modules/@metro-labs/http/src/connector-oauth.ts +1 -1
  83. package/runtime/node_modules/@metro-labs/http/src/connector-policy.ts +1 -6
  84. package/runtime/node_modules/@metro-labs/http/src/connector-public-url.ts +4 -2
  85. package/runtime/node_modules/@metro-labs/http/src/connector-tools.ts +1 -11
  86. package/runtime/node_modules/@metro-labs/http/src/connector-url.ts +0 -6
  87. package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +1 -1
  88. package/runtime/node_modules/@metro-labs/http/src/mcp-server-tools.ts +0 -2
  89. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +1 -1
  90. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +2 -2
  91. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +6 -7
  92. package/runtime/node_modules/@metro-labs/inference/package.json +1 -0
  93. package/runtime/node_modules/@metro-labs/inference/src/attempts.ts +3 -3
  94. package/runtime/node_modules/@metro-labs/inference/src/bedrock.ts +1 -1
  95. package/runtime/node_modules/@metro-labs/inference/src/codex-translate.ts +2 -2
  96. package/runtime/node_modules/@metro-labs/inference/src/delivered.ts +1 -1
  97. package/runtime/node_modules/@metro-labs/inference/src/effort.ts +0 -6
  98. package/runtime/node_modules/@metro-labs/inference/src/fallback.ts +7 -5
  99. package/runtime/node_modules/@metro-labs/inference/src/forward.ts +5 -5
  100. package/runtime/node_modules/@metro-labs/inference/src/measure.ts +2 -3
  101. package/runtime/node_modules/@metro-labs/inference/src/named.ts +3 -3
  102. package/runtime/node_modules/@metro-labs/inference/src/probes.ts +98 -0
  103. package/runtime/node_modules/@metro-labs/inference/src/usage.ts +5 -5
  104. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +6 -8
  105. package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +2 -1
  106. package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +3 -5
  107. package/runtime/node_modules/@metro-labs/outlook/src/me.ts +6 -6
  108. package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +10 -13
  109. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +7 -26
  110. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +1 -1
  111. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +3 -6
  112. package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +3 -2
  113. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +6 -20
  114. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +1 -7
  115. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +0 -2
  116. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +1 -16
  117. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +1 -8
  118. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +2 -1
  119. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +1 -8
  120. package/runtime/node_modules/@metro-labs/threema/src/chats.ts +4 -3
  121. package/runtime/node_modules/@metro-labs/threema/src/format.ts +15 -17
  122. package/runtime/node_modules/@metro-labs/threema/src/groups.ts +2 -4
  123. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +2 -1
  124. package/runtime/node_modules/@metro-labs/threema/src/messages.ts +2 -1
  125. package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +7 -7
  126. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +1 -3
  127. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +0 -2
  128. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +1 -1
  129. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +1 -1
  130. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +7 -0
  131. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +4 -10
  132. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -5
  133. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +14 -26
  134. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +1 -7
  135. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +17 -58
  136. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +1 -6
  137. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +10 -31
  138. package/runtime/node_modules/@metro-labs/xmtp/src/frames.ts +12 -14
  139. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +2 -3
  140. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +3 -9
  141. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +8 -9
  142. package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +4 -2
  143. package/runtime/node_modules/@metro-labs/xmtp/src/network.ts +2 -3
  144. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +2 -1
  145. package/runtime/node_modules/@metro-labs/xmtp/src/read-attachments.ts +2 -2
  146. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +2 -1
  147. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +23 -46
  148. package/runtime/node_modules/@metro-labs/xmtp/src/wallet.ts +5 -4
  149. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +12 -15
  150. package/runtime/runtime.json +1 -1
  151. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
  152. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
  153. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/lines.ts +2 -8
  154. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/protocol.ts +0 -6
  155. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
  156. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
  157. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
  158. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
  159. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
  160. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
  161. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
  162. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
  163. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
  164. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
  165. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/train-error.ts +4 -4
  166. package/runtime/sdk-runner/runner.json +1 -1
  167. package/runtime/sdk-runner/src/approvals.ts +2 -2
  168. package/runtime/sdk-runner/src/automation-command.ts +2 -2
  169. package/runtime/sdk-runner/src/calls.ts +0 -6
  170. package/runtime/sdk-runner/src/errors.ts +1 -1
  171. package/runtime/sdk-runner/src/link.ts +1 -1
  172. package/runtime/sdk-runner/src/runner.ts +2 -2
  173. package/runtime/sdk-runner/src/session-watch.ts +5 -34
  174. package/runtime/sdk-runner/src/task-state.ts +1 -1
  175. package/runtime/sdk-runner/src/tool-proxy.ts +1 -1
  176. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +0 -40
@@ -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),
@@ -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(
@@ -43,7 +43,7 @@ function helpersBody(setup: ModelSetup): Record<string, unknown> | null {
43
43
  const helpers = helperSetup(setup);
44
44
  if (helpers === null) return null;
45
45
  const primary = primaryRoute(helpers);
46
- return { route: primary?.connection.id ?? '', model: primary?.model ?? '', fallbacks: helpers.fallbacks, chain: chainStatus(helpers) };
46
+ return { route: primary?.connection.id ?? '', model: primary?.model ?? '', fallbacks: helpers.fallbacks, chain: chainStatus(helpers, Date.now(), setupNotReady) };
47
47
  }
48
48
 
49
49
  export function settingsBody(deps: ModelApiDeps): Record<string, unknown> {
@@ -59,7 +59,7 @@ export function settingsBody(deps: ModelApiDeps): Record<string, unknown> {
59
59
  reason,
60
60
  connections: setup.connections.map(publicConnection),
61
61
  fallbacks: setup.fallbacks,
62
- chain: chainStatus(setup),
62
+ chain: chainStatus(setup, Date.now(), setupNotReady),
63
63
  helpers: helpersBody(setup),
64
64
  effort: { ...EFFORT_DEFAULTS, ...setup.effort },
65
65
  };
@@ -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
  );
@@ -0,0 +1,26 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+
4
+ export const TAILSCALED_DEFAULTS = '/etc/default/tailscaled';
5
+ const NO_FORWARDING = 'TS_SSH_DISABLE_FORWARDING=1';
6
+
7
+ type Run = (file: string, args: string[]) => boolean;
8
+
9
+ const quiet: Run = (file, args) => spawnSync(file, args, { stdio: 'ignore' }).status === 0;
10
+
11
+ export function withoutForwarding(text: string): string | null {
12
+ if (text.split('\n').some((line) => line.trim() === NO_FORWARDING)) return null;
13
+ return `${text}${text === '' || text.endsWith('\n') ? '' : '\n'}${NO_FORWARDING}\n`;
14
+ }
15
+
16
+ export function hardenTailscale(run: Run = quiet, path: string = TAILSCALED_DEFAULTS): string[] {
17
+ if (!run('tailscale', ['version'])) return ['tailscale is not installed here, nothing to harden'];
18
+ const notes: string[] = [];
19
+ const next = withoutForwarding(existsSync(path) ? readFileSync(path, 'utf8') : '');
20
+ if (next !== null) {
21
+ writeFileSync(path, next, { mode: 0o644 });
22
+ notes.push(run('systemctl', ['restart', 'tailscaled']) ? 'Tailscale SSH forwarding is off' : `wrote ${NO_FORWARDING}, but tailscaled did not restart; it applies at its next start`);
23
+ }
24
+ if (!run('tailscale', ['set', '--auto-update'])) notes.push('tailscale set --auto-update failed; update Tailscale by hand');
25
+ return notes;
26
+ }
@@ -10,8 +10,6 @@ import {
10
10
 
11
11
  const KEEPALIVE_MS = 25_000;
12
12
 
13
- const monitorEnabled = (): boolean => hasAnyKey();
14
-
15
13
  function cors(req: IncomingMessage): Record<string, string> {
16
14
  return {
17
15
  'access-control-allow-origin': req.headers.origin ?? '*',
@@ -104,7 +102,7 @@ function routeApi(
104
102
  }
105
103
 
106
104
  function preflight(req: IncomingMessage, res: ServerResponse): boolean {
107
- if (!monitorEnabled()) {
105
+ if (!hasAnyKey()) {
108
106
  sendJson(res, req, 404, { error: 'not found' });
109
107
  return true;
110
108
  }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { ApiError } from '@metro-labs/http/api-error';
3
3
  import { stringOf } from '@metro-labs/http/api-http';
4
+ import { isRecord } from '@metro-labs/core/is-record';
4
5
  import { publicBaseUrl } from '../files/attach-serve.js';
5
6
  import { localOwner, readLocalAgentFile, localRenewGmail } from '../agents/file-admin.js';
6
7
  import { startManagedGmail } from '../stations/attach-gmail.js';
@@ -57,9 +58,8 @@ export const startGmailAttach: StartAttach = async (_station, input, hooks, owne
57
58
  };
58
59
 
59
60
  export function finishGmailUpgrade(owner: AttachOwner, config: Record<string, unknown>): ReturnType<typeof localRenewGmail> | null {
60
- const value = config.gmailUpgrade;
61
- if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
62
- const upgrade = value as Record<string, unknown>;
61
+ const upgrade = config.gmailUpgrade;
62
+ if (!isRecord(upgrade)) return null;
63
63
  assertGmailNotDeleting(owner.agentId, stringOf(upgrade.accountId));
64
64
  const saved = { ...config };
65
65
  delete saved.gmailUpgrade;
@@ -11,6 +11,7 @@ import {
11
11
  } from 'node:http';
12
12
  import { Line } from '@metro-labs/core/lines';
13
13
  import { MCP_STATUS_PATH } from '../mcp/diagnostics.js';
14
+ import { capSize } from '../channels/pending.js';
14
15
  import { errMsg, log } from '@metro-labs/core/log';
15
16
  import {
16
17
  classifyEvent,
@@ -50,7 +51,7 @@ export interface DedupSeq {
50
51
  }
51
52
 
52
53
  export function makeDedupSeq(): DedupSeq {
53
- const seen = new Map<string, true>();
54
+ const seen = new Set<string>();
54
55
  const seqByLine = new Map<string, number>();
55
56
 
56
57
  log.info('dedup+seq: live-from-boot (no persisted seed)');
@@ -72,20 +73,12 @@ export function makeDedupSeq(): DedupSeq {
72
73
  );
73
74
  return null;
74
75
  }
75
- seen.set(key, true);
76
- while (seen.size > LRU_CAP) {
77
- const oldest = seen.keys().next();
78
- if (oldest.done) break;
79
- seen.delete(oldest.value);
80
- }
76
+ seen.add(key);
77
+ capSize(seen, LRU_CAP);
81
78
  }
82
79
  const next = (seqByLine.get(entry.line) ?? 0) + 1;
83
80
  seqByLine.set(entry.line, next);
84
- while (seqByLine.size > LRU_CAP) {
85
- const oldest = seqByLine.keys().next();
86
- if (oldest.done) break;
87
- seqByLine.delete(oldest.value);
88
- }
81
+ capSize(seqByLine, LRU_CAP);
89
82
  return next;
90
83
  },
91
84
  };
@@ -318,18 +311,10 @@ async function handleWebhookRoute(
318
311
  return true;
319
312
  }
320
313
 
321
- function handleSignInRoutes(
322
- req: IncomingMessage,
323
- res: ServerResponse,
324
- apis: SessionApis,
325
- ): boolean {
326
- return apis.mode !== undefined && handleModeRequest(req, res, apis.mode);
327
- }
328
-
329
314
  function handleEarlyRoutes(req: IncomingMessage, res: ServerResponse, apis: SessionApis): boolean {
330
315
  if (handleHealth(req, res)) return true;
331
316
  if (apis.gateway?.(req, res) === true) return true;
332
- if (handleSignInRoutes(req, res, apis)) return true;
317
+ if (apis.mode !== undefined && handleModeRequest(req, res, apis.mode)) return true;
333
318
  return handleSessionApis(req, res, apis);
334
319
  }
335
320
 
@@ -115,10 +115,6 @@ function bundleApi(deps: LocalModeDeps): BundleApiDeps {
115
115
  };
116
116
  }
117
117
 
118
- function localModeInfo(): ModeInfo {
119
- return { mode: 'local', owner: localOwner(), version: METRO_VERSION };
120
- }
121
-
122
118
  async function accountCall(
123
119
  station: StationName,
124
120
  action: string,
@@ -150,6 +146,6 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
150
146
  modelApi: {},
151
147
  gateway: boxGatewayHandler(),
152
148
  terminalApi: {},
153
- mode: localModeInfo,
149
+ mode: (): ModeInfo => ({ mode: 'local', owner: localOwner(), version: METRO_VERSION }),
154
150
  };
155
151
  }
@@ -64,7 +64,7 @@ async function fromPathAs(
64
64
  path: string,
65
65
  ): Promise<ResolvedAttachment> {
66
66
  const proc = Bun.spawn(agentCommand(['cat', '--', path]), { stdout: 'pipe', stderr: 'pipe' });
67
- const name = a.name ?? basenameOf(path) ?? 'attachment';
67
+ const name = a.name ?? basenameOf(path);
68
68
  let saved: Awaited<ReturnType<typeof streamToTemp>>;
69
69
  try {
70
70
  saved = await streamToTemp(proc.stdout, name);
@@ -101,7 +101,7 @@ async function fromPath(
101
101
  return {
102
102
  path,
103
103
  mime: a.mime ?? guessMime(path),
104
- name: a.name ?? basenameOf(path) ?? 'attachment',
104
+ name: a.name ?? basenameOf(path),
105
105
  bytes: file.size,
106
106
  };
107
107
  }
@@ -129,7 +129,7 @@ async function fromUrlAs(
129
129
  url: string,
130
130
  ): Promise<ResolvedAttachment> {
131
131
  const proc = Bun.spawn(agentCommand(curlArgs(url)), { stdout: 'pipe', stderr: 'pipe' });
132
- const name = a.name ?? basenameOf(url) ?? 'attachment';
132
+ const name = a.name ?? basenameOf(url);
133
133
  const said = new Response(proc.stderr).text().catch((err: unknown) => {
134
134
  log.debug({ err: String(err) }, 'send: could not read what curl said');
135
135
  return '';
@@ -167,7 +167,7 @@ async function fromUrl(
167
167
  assertContentLength(res.headers.get('content-length'));
168
168
  const mime =
169
169
  a.mime ?? res.headers.get('content-type')?.split(';')[0] ?? guessMime(url);
170
- const name = a.name ?? basenameOf(url) ?? 'attachment';
170
+ const name = a.name ?? basenameOf(url);
171
171
  if (res.body === null) throw new Error(`attachment fetch returned no body for '${url}'`);
172
172
  const { dir, path, bytes } = await streamToTemp(res.body, name);
173
173
  return { path, mime, name, bytes, temp: dir };
@@ -223,8 +223,8 @@ const sourcesOf = (a: CanonicalAttachment): string[] =>
223
223
 
224
224
  async function resolveAttachment(
225
225
  a: CanonicalAttachment,
226
- budget: InlineBudget = { used: 0 },
227
- opts: ResolveOptions = {},
226
+ budget: InlineBudget,
227
+ opts: ResolveOptions,
228
228
  ): Promise<ResolvedAttachment> {
229
229
  const sources = sourcesOf(a);
230
230
  if (sources.length === 0)
@@ -217,7 +217,6 @@ async function prepareThreema(input: AttachInput): Promise<PreparedAccount> {
217
217
  },
218
218
  };
219
219
  } catch (err) {
220
- if (err instanceof StationAttachError) throw err;
221
220
  return rejected(
222
221
  err,
223
222
  err instanceof ThreemaGatewayError,