@stage-labs/metro 0.1.0-beta.201 → 0.1.0-beta.202

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 (163) hide show
  1. package/dist/claude.js +4 -15
  2. package/dist/cli.js +5 -3
  3. package/dist/hold.js +1 -2
  4. package/dist/runtime-install.js +4 -5
  5. package/dist/runtime.js +1 -4
  6. package/dist/serve.js +4 -5
  7. package/dist/service.js +82 -21
  8. package/dist/update.js +1 -1
  9. package/package.json +2 -2
  10. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
  11. package/runtime/node_modules/@metro-labs/core/src/events.ts +4 -2
  12. package/runtime/node_modules/@metro-labs/core/src/lines.ts +12 -25
  13. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +45 -22
  14. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +0 -1
  15. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +8 -80
  16. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +3 -5
  17. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +4 -9
  18. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +9 -39
  19. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +1 -1
  20. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +9 -28
  21. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +0 -1
  22. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +3 -3
  23. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +8 -116
  24. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +11 -35
  25. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +7 -9
  26. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +1 -1
  27. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +1 -1
  28. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +1 -1
  29. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +1 -1
  30. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -3
  31. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +2 -2
  32. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +3 -29
  33. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +5 -8
  34. package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +1 -1
  35. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +5 -8
  36. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +3 -6
  37. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +25 -3
  38. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +2 -3
  39. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -2
  40. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +1 -1
  41. package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +6 -56
  42. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +3 -3
  43. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -1
  44. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +7 -6
  45. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +1 -1
  46. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +7 -7
  47. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -4
  48. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +1 -1
  49. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +2 -4
  50. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +19 -22
  51. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +1 -2
  52. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +6 -8
  53. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +3 -6
  54. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +1 -1
  55. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +2 -3
  56. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +28 -1
  57. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  58. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +4 -4
  59. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +1 -1
  60. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +1 -1
  61. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +1 -1
  62. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +3 -2
  63. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +1 -1
  64. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +2 -2
  65. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +1 -1
  66. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +3 -3
  67. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +1 -1
  68. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +3 -3
  69. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +6 -6
  70. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +7 -7
  71. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +1 -1
  72. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -2
  73. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +1 -1
  74. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +10 -31
  75. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +1 -1
  76. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +1 -1
  77. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +2 -2
  78. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
  79. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +1 -2
  80. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +2 -2
  81. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  82. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +3 -3
  83. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +2 -2
  84. package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +2 -2
  85. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +2 -2
  86. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -2
  88. package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +1 -1
  89. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -4
  90. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +3 -8
  91. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +2 -2
  92. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +2 -2
  93. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +2 -2
  94. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +6 -7
  95. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +1 -1
  96. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +2 -2
  97. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +2 -2
  98. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +1 -1
  99. package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +5 -44
  100. package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +4 -4
  101. package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +6 -7
  102. package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +12 -13
  103. package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +5 -22
  104. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +1 -19
  105. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +7 -35
  106. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +5 -10
  107. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +1 -1
  108. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +1 -2
  109. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +1 -1
  110. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +11 -12
  111. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +2 -5
  112. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +0 -1
  113. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +1 -15
  114. package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +0 -6
  115. package/runtime/node_modules/@metro-labs/outlook/src/format.ts +0 -1
  116. package/runtime/node_modules/@metro-labs/outlook/src/state.ts +1 -1
  117. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +4 -22
  118. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +2 -2
  119. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +1 -3
  120. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +1 -1
  121. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +1 -2
  122. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +1 -1
  123. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +4 -37
  124. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +7 -23
  125. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +1 -4
  126. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +4 -9
  127. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +3 -3
  128. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -11
  129. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +2 -12
  130. package/runtime/node_modules/@metro-labs/threema/src/files.ts +4 -15
  131. package/runtime/node_modules/@metro-labs/threema/src/format.ts +2 -7
  132. package/runtime/node_modules/@metro-labs/whatsapp/package.json +1 -2
  133. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +5 -26
  134. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +5 -6
  135. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -3
  136. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +1 -1
  137. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -12
  138. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +1 -1
  139. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +14 -5
  140. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +1 -1
  141. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +31 -78
  142. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -4
  143. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +1 -6
  144. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +1 -1
  145. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +1 -1
  146. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +1 -1
  147. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +2 -2
  148. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
  149. package/runtime/runtime.json +1 -1
  150. package/runtime/node_modules/@metro-labs/core/src/identity.ts +0 -7
  151. package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +0 -2
  152. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +0 -12
  153. package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +0 -13
  154. package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +0 -1
  155. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +0 -29
  156. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +0 -43
  157. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +0 -165
  158. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -1
  159. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +0 -22
  160. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -1
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -1
  162. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
  163. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -1
@@ -34,14 +34,14 @@ export interface Route {
34
34
 
35
35
  export class ModelConfigError extends Error {}
36
36
 
37
- export const MODEL_FILE = 'model.json';
37
+ const MODEL_FILE = 'model.json';
38
38
  const MAX_FIELD = 512;
39
39
  const MAX_LABEL = 60;
40
40
  const MAX_CONNECTIONS = 20;
41
41
  const PREFIX_RE = /^(anthropic|bedrock|openrouter|codex|gemini):(.+)$/;
42
42
  const SMALL_RE = /haiku/i;
43
43
 
44
- export const LABELS: Record<Provider, string> = {
44
+ const LABELS: Record<Provider, string> = {
45
45
  anthropic: 'Anthropic',
46
46
  bedrock: 'Amazon Bedrock',
47
47
  openrouter: 'OpenRouter',
@@ -56,7 +56,7 @@ const maybe = (value: unknown): string | null => (typeof value === 'string' && v
56
56
 
57
57
  export const empty = (): ModelConfig => ({ version: 2, route: '', connections: [] });
58
58
 
59
- export const newConnection = (provider: Provider, label: string): Connection => ({
59
+ const newConnection = (provider: Provider, label: string): Connection => ({
60
60
  id: newId(),
61
61
  provider,
62
62
  label,
@@ -102,36 +102,15 @@ function connectionFromDisk(raw: unknown): Connection | null {
102
102
  };
103
103
  }
104
104
 
105
- function fromVersionOne(raw: Record<string, unknown>): ModelConfig {
106
- const block = (name: Provider): Record<string, unknown> => (isRecord(raw[name]) ? raw[name] : {});
107
- const made: Connection[] = [];
108
- const keep = (provider: Provider, fill: (c: Connection) => Connection, has: boolean): void => {
109
- if (has) made.push(fill(newConnection(provider, LABELS[provider])));
110
- };
111
- const anthropic = block('anthropic');
112
- const bedrock = block('bedrock');
113
- const openrouter = block('openrouter');
114
- const codex = block('codex');
115
- const gemini = block('gemini');
116
- keep('anthropic', (c) => ({ ...c, apiKey: text(anthropic.apiKey), model: text(anthropic.model) }), text(anthropic.apiKey) !== '' || text(anthropic.model) !== '');
117
- keep('bedrock', (c) => ({ ...c, apiKey: text(bedrock.apiKey), region: text(bedrock.region), model: text(bedrock.model) }), text(bedrock.apiKey) !== '');
118
- keep('openrouter', (c) => ({ ...c, apiKey: text(openrouter.apiKey), model: text(openrouter.model), zdr: openrouter.zdr === true }), text(openrouter.apiKey) !== '');
119
- keep('codex', (c) => ({ ...c, model: text(codex.model), codex: codexFromDisk(codex.auth) }), codexFromDisk(codex.auth) !== null);
120
- keep('gemini', (c) => ({ ...c, model: text(gemini.model), gemini: geminiTokensFromDisk(gemini.auth) }), geminiTokensFromDisk(gemini.auth) !== null);
121
- const wanted = isProvider(raw.provider) ? raw.provider : 'anthropic';
122
- return { version: 2, route: made.find((c) => c.provider === wanted)?.id ?? '', connections: made };
123
- }
124
-
125
105
  export function parseModelConfig(raw: unknown): ModelConfig {
126
- if (!isRecord(raw)) return empty();
127
- if (!Array.isArray(raw.connections)) return fromVersionOne(raw);
106
+ if (!isRecord(raw) || !Array.isArray(raw.connections)) return empty();
128
107
  const connections = raw.connections.flatMap((c: unknown) => connectionFromDisk(c) ?? []).slice(0, MAX_CONNECTIONS);
129
108
  const route = text(raw.route);
130
109
  return { version: 2, route: connections.some((c) => c.id === route) ? route : (connections[0]?.id ?? ''), connections };
131
110
  }
132
111
 
133
112
  const needsIds = (raw: unknown): boolean =>
134
- isRecord(raw) && (!Array.isArray(raw.connections) || raw.connections.some((c: unknown) => isRecord(c) && text(c.id) === ''));
113
+ isRecord(raw) && Array.isArray(raw.connections) && raw.connections.some((c: unknown) => isRecord(c) && text(c.id) === '');
135
114
 
136
115
  export function readModelConfig(dir = agentsDir()): ModelConfig {
137
116
  const raw = readJson<unknown>(join(dir, MODEL_FILE), null, { warn: 'model-config: model.json is unreadable, so every request goes to Anthropic until it is fixed' });
@@ -148,7 +127,7 @@ export const connectionOf = (cfg: ModelConfig, id: string): Connection | null =>
148
127
 
149
128
  export const routedConnection = (cfg: ModelConfig): Connection | null => connectionOf(cfg, cfg.route);
150
129
 
151
- export function requireConnection(cfg: ModelConfig, id: string): Connection {
130
+ function requireConnection(cfg: ModelConfig, id: string): Connection {
152
131
  const found = connectionOf(cfg, id);
153
132
  if (found === null) throw new ModelConfigError('no such connection');
154
133
  return found;
@@ -181,7 +160,7 @@ export function notReady(cfg: ModelConfig, conn = routedConnection(cfg)): string
181
160
  return CHECKS[conn.provider].find(([missing]) => missing(conn))?.[1] ?? null;
182
161
  }
183
162
 
184
- export const isSmallModel = (requested: string): boolean => SMALL_RE.test(requested);
163
+ const isSmallModel = (requested: string): boolean => SMALL_RE.test(requested);
185
164
 
186
165
  const DEFAULTS: Record<Provider, (requested: string, c: Connection) => string> = {
187
166
  openrouter: (requested, c) => (requested.includes('/') ? requested : c.model),
@@ -210,7 +189,7 @@ export function resolveRoute(requested: string, cfg: ModelConfig): Route | null
210
189
  export const routeLabel = (route: Route): string =>
211
190
  route.connection.provider === 'anthropic' ? route.model : `${route.connection.provider}:${route.model}`;
212
191
 
213
- export function publicConnection(c: Connection): Record<string, unknown> {
192
+ function publicConnection(c: Connection): Record<string, unknown> {
214
193
  return {
215
194
  id: c.id,
216
195
  provider: c.provider,
@@ -256,7 +235,7 @@ function flag(patch: Record<string, unknown>, key: string, current: boolean, wha
256
235
  return value;
257
236
  }
258
237
 
259
- export function labelFor(cfg: ModelConfig, provider: Provider, given: string): string {
238
+ function labelFor(cfg: ModelConfig, provider: Provider, given: string): string {
260
239
  if (given !== '') return given.slice(0, MAX_LABEL);
261
240
  const taken = cfg.connections.filter((c) => c.provider === provider).length;
262
241
  return taken === 0 ? LABELS[provider] : `${LABELS[provider]} ${String(taken + 1)}`;
@@ -270,7 +249,7 @@ export function addConnection(cfg: ModelConfig, patch: unknown): ModelConfig {
270
249
  return { ...cfg, route: cfg.route === '' ? filled.id : cfg.route, connections: [...cfg.connections, filled] };
271
250
  }
272
251
 
273
- export function applyToConnection(c: Connection, patch: Record<string, unknown>): Connection {
252
+ function applyToConnection(c: Connection, patch: Record<string, unknown>): Connection {
274
253
  return {
275
254
  ...c,
276
255
  label: field(patch, 'label', c.label, 'label', MAX_LABEL),
@@ -104,7 +104,7 @@ export const CODEX_ROUTES: Record<string, Route> = {
104
104
  },
105
105
  };
106
106
 
107
- export const geminiDeps = (deps: ModelApiDeps, store: Store): GeminiDeps => ({
107
+ const geminiDeps = (deps: ModelApiDeps, store: Store): GeminiDeps => ({
108
108
  base: deps.geminiBase,
109
109
  tokenBase: deps.geminiTokenBase,
110
110
  fetchImpl: deps.fetchImpl,
@@ -31,7 +31,7 @@ export interface Store {
31
31
  write: (cfg: ModelConfig) => void;
32
32
  }
33
33
 
34
- export type Handler = (req: IncomingMessage, deps: ModelApiDeps, store: Store) => Promise<unknown>;
34
+ type Handler = (req: IncomingMessage, deps: ModelApiDeps, store: Store) => Promise<unknown>;
35
35
 
36
36
  export interface Route {
37
37
  method: 'GET' | 'POST';
@@ -1,6 +1,6 @@
1
1
  import { isRecord } from '@metro-labs/core/is-record';
2
2
  import { GatewayError } from './forward.js';
3
- import { stringOf } from './text.js';
3
+ import { stringOf } from '@metro-labs/http/api-http';
4
4
 
5
5
  export const OPENROUTER_BASE = 'https://openrouter.ai/api';
6
6
  const MODELS_MAX = 2000;
@@ -37,7 +37,7 @@ function modelOf(entry: unknown): OpenRouterModel | null {
37
37
  };
38
38
  }
39
39
 
40
- export function newestFirst(a: OpenRouterModel, b: OpenRouterModel): number {
40
+ function newestFirst(a: OpenRouterModel, b: OpenRouterModel): number {
41
41
  const when = (b.created ?? 0) - (a.created ?? 0);
42
42
  return when !== 0 ? when : a.id.localeCompare(b.id);
43
43
  }
@@ -1,9 +1,9 @@
1
1
  import { isRecord } from '@metro-labs/core/is-record';
2
2
  import { GatewayError } from './forward.js';
3
3
  import type { Connection } from './model-config.js';
4
- import { stringOf } from './text.js';
4
+ import { stringOf } from '@metro-labs/http/api-http';
5
5
 
6
- export const ANTHROPIC_API = 'https://api.anthropic.com';
6
+ const ANTHROPIC_API = 'https://api.anthropic.com';
7
7
  const ANTHROPIC_VERSION = '2023-06-01';
8
8
  const LIST_MAX = 1000;
9
9
 
@@ -12,7 +12,7 @@ export interface ProviderModel {
12
12
  name: string;
13
13
  }
14
14
 
15
- export const KNOWN_CLAUDE: ProviderModel[] = [
15
+ const KNOWN_CLAUDE: ProviderModel[] = [
16
16
  { id: 'claude-fable-5-1', name: 'Claude Fable 5.1' },
17
17
  { id: 'claude-opus-5-5', name: 'Claude Opus 5.5' },
18
18
  { id: 'claude-opus-5', name: 'Claude Opus 5' },
@@ -1,9 +1,8 @@
1
1
  import { isRecord } from '@metro-labs/core/is-record';
2
+ import { stringOf } from '@metro-labs/http/api-http';
2
3
 
3
4
  const IMAGE_NOTE = '[an image was attached here; this model cannot see it]';
4
5
 
5
- export const stringOf = (value: unknown): string => (typeof value === 'string' ? value : '');
6
-
7
6
  export const nonEmpty = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
8
7
 
9
8
  const partText = (part: Record<string, unknown>): string => (part.type === 'text' ? stringOf(part.text) : part.type === 'image' ? IMAGE_NOTE : '');
@@ -1,13 +1,13 @@
1
1
  type Key = string;
2
2
 
3
- export interface UsageWindow {
3
+ interface UsageWindow {
4
4
  label: string;
5
5
  used: number | null;
6
6
  resetAt: string | null;
7
7
  detail: string | null;
8
8
  }
9
9
 
10
- export interface Tally {
10
+ interface Tally {
11
11
  requests: number;
12
12
  input: number;
13
13
  output: number;
@@ -3,14 +3,13 @@ import type { ToolResult } from '@metro-labs/core/stations/types';
3
3
  import { accountFromLine, knownAccounts } from '../agents/map.js';
4
4
  import { policyFor, storedPolicy, strictest, type Access, type PolicyTarget } from '../policy/policy.js';
5
5
  import { errResult } from './ctx.js';
6
- import { takeGrant } from '../approvals/pending.js';
7
- import { NEEDS_APPROVAL } from '../approvals/needs.js';
6
+ import { NEEDS_APPROVAL, takeGrant } from '../approvals/pending.js';
8
7
  import { connectorToolOf } from '../connectors/gates.js';
9
8
  import { profileScopeLine } from './profile-lookup.js';
10
9
  import { stationOfAccount } from './read-tool.js';
11
10
  import { stationForTool, toolGroupOf } from './tool-catalog.js';
12
11
 
13
- export type ChannelTarget = Extract<PolicyTarget, { kind: 'channel' }>;
12
+ type ChannelTarget = Extract<PolicyTarget, { kind: 'channel' }>;
14
13
 
15
14
  export const UNGATED = new Set<string>(['list_accounts', 'create_upload']);
16
15
 
@@ -19,7 +18,7 @@ function fromLine(line: string, override: string | undefined): ChannelTarget[] {
19
18
  return acct === undefined ? [] : [{ kind: 'channel', station: acct.station, account: override ?? acct.accountId }];
20
19
  }
21
20
 
22
- export function channelTargets(name: string, a: Record<string, unknown>): ChannelTarget[] {
21
+ function channelTargets(name: string, a: Record<string, unknown>): ChannelTarget[] {
23
22
  if (UNGATED.has(name)) return [];
24
23
  if (name === 'get_profile') {
25
24
  const line = profileScopeLine(a);
@@ -60,13 +59,13 @@ export function policyGate(name: string, a: Record<string, unknown>): ToolResult
60
59
  return takeGrant(isChannelTool(name), a) ? undefined : errResult(NEEDS_APPROVAL(target.station, name));
61
60
  }
62
61
 
63
- export interface EffectivePolicy {
62
+ interface EffectivePolicy {
64
63
  read: Access;
65
64
  write: Access;
66
65
  tools: Record<string, Access>;
67
66
  }
68
67
 
69
- export function effectivePolicy(target: ChannelTarget): EffectivePolicy {
68
+ function effectivePolicy(target: ChannelTarget): EffectivePolicy {
70
69
  const stored = storedPolicy(target);
71
70
  return { read: stored?.read ?? 'allow', write: stored?.write ?? 'allow', tools: { ...stored?.tools } };
72
71
  }
@@ -10,7 +10,7 @@ import { UNGATED } from './policy-gate.js';
10
10
  import { connectorGates } from '../connectors/gates.js';
11
11
  import { stationToolOwners, TOOL_DEFS, toolGroupOf } from './tool-catalog.js';
12
12
 
13
- export interface PolicySnapshot {
13
+ interface PolicySnapshot {
14
14
  version: 1;
15
15
  tools: Record<string, ToolGroup>;
16
16
  owners: Record<string, string>;
@@ -31,7 +31,7 @@ const FILE = 'policy.json';
31
31
 
32
32
  export const policySnapshotPath = (dir = agentsDir()): string => join(dir, FILE);
33
33
 
34
- export function policySnapshot(): PolicySnapshot {
34
+ function policySnapshot(): PolicySnapshot {
35
35
  const accounts: Record<string, ToolPolicy> = {};
36
36
  for (const { target, policy } of storedPolicies())
37
37
  if (target.kind === 'channel') accounts[`${target.station}/${target.account}`] = policy;
@@ -60,7 +60,7 @@ function current(path: string): string | undefined {
60
60
  }
61
61
  }
62
62
 
63
- export function writePolicySnapshot(path = policySnapshotPath()): void {
63
+ function writePolicySnapshot(path = policySnapshotPath()): void {
64
64
  const text = `${JSON.stringify(policySnapshot(), null, 2)}\n`;
65
65
  if (current(path) === text) return;
66
66
  try {
@@ -36,11 +36,11 @@ export function profileTarget(a: Record<string, unknown>): ProfileTarget | { ref
36
36
  if (!from) return { refused: 'get_profile requires `from`' };
37
37
  const parsed = Line.parse(from);
38
38
  const at = parsed?.path.indexOf('user') ?? -1;
39
- if (parsed === null || at < 0 || at + 1 >= parsed.path.length) return { refused: `${from} is not a person: expected metro://<station>/<account>/user/<id>` };
39
+ if (parsed === null || at < 1 || at + 1 >= parsed.path.length) return { refused: `${from} is not a person: expected metro://<station>/<account>/user/<id>` };
40
40
  const station = stationByName(parsed.station);
41
41
  if (!station) return { refused: `no station named ${parsed.station}` };
42
42
  if (station.readsProfiles !== true) return { refused: `${station.name} has no profile metro can read` };
43
- return { station, account: at === 0 ? 'default' : parsed.path.slice(0, at).join('/'), user: parsed.path.slice(at + 1).join('/') };
43
+ return { station, account: parsed.path.slice(0, at).join('/'), user: parsed.path.slice(at + 1).join('/') };
44
44
  }
45
45
 
46
46
  export const profileScopeLine = (a: Record<string, unknown>): string | undefined => {
@@ -22,7 +22,7 @@ export function stationOfAccount(account: string): string | undefined {
22
22
  return hits.length === 1 ? hits[0]?.station : undefined;
23
23
  }
24
24
 
25
- export function readArgs(a: Record<string, unknown>): Record<string, unknown> {
25
+ function readArgs(a: Record<string, unknown>): Record<string, unknown> {
26
26
  const args: Record<string, unknown> = {};
27
27
  if (str(a.line)) args.line = str(a.line);
28
28
  if (str(a.account)) args.account = str(a.account);
@@ -34,7 +34,7 @@ export function readArgs(a: Record<string, unknown>): Record<string, unknown> {
34
34
  return args;
35
35
  }
36
36
 
37
- export function ignoredFilters(station: Station, a: Record<string, unknown>): string[] {
37
+ function ignoredFilters(station: Station, a: Record<string, unknown>): string[] {
38
38
  const supported = station.readFilters ?? new Set<ReadFilter>();
39
39
  return FILTERS.filter(([snake, filter]) => given(a[snake]) && !supported.has(filter)).map(([snake]) => snake);
40
40
  }
@@ -1,8 +1,8 @@
1
1
  export const HELPER_PATH = '/usr/local/lib/metro/root-helper';
2
- export const HELPER_VERSION = 1;
2
+ const HELPER_VERSION = 1;
3
3
  export const SUDOERS_PATH = '/etc/sudoers.d/metro';
4
4
  export const METRO_USER = 'metro';
5
- export const METRO_HOME = '/var/lib/metro';
5
+ const METRO_HOME = '/var/lib/metro';
6
6
 
7
7
  export const sudoersText = (agent: string): string =>
8
8
  [
@@ -0,0 +1,37 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { HELPER_PATH, helperScript, SUDOERS_PATH, sudoersText } from './helper-script.js';
6
+ import { AGENT_NAME } from '../agent-user/user.js';
7
+
8
+ interface InstallPaths {
9
+ helper: string;
10
+ sudoers: string;
11
+ }
12
+
13
+ function checked(file: string, args: string[]): void {
14
+ const run = spawnSync(file, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
15
+ if (run.error !== undefined) throw run.error;
16
+ if (run.status !== 0) throw new Error(`${file} ${args.join(' ')}: ${`${run.stdout}${run.stderr}`.trim() || `exit ${String(run.status)}`}`);
17
+ }
18
+
19
+ export function installRootHelper(paths: InstallPaths = { helper: HELPER_PATH, sudoers: SUDOERS_PATH }): void {
20
+ mkdirSync(dirname(paths.helper), { recursive: true, mode: 0o755 });
21
+ writeFileSync(paths.helper, helperScript(), { mode: 0o755 });
22
+ chmodSync(paths.helper, 0o755);
23
+ const work = mkdtempSync(join(tmpdir(), 'metro-sudoers-'));
24
+ try {
25
+ const draft = join(work, 'metro');
26
+ writeFileSync(draft, sudoersText(AGENT_NAME), { mode: 0o440 });
27
+ checked('visudo', ['-cf', draft]);
28
+ checked('install', ['-m', '440', draft, paths.sudoers]);
29
+ } finally {
30
+ rmSync(work, { recursive: true, force: true });
31
+ }
32
+ }
33
+
34
+ if (import.meta.main) {
35
+ installRootHelper();
36
+ process.stdout.write(`metro: wrote ${HELPER_PATH} and ${SUDOERS_PATH}\n`);
37
+ }
@@ -83,7 +83,7 @@ function modeThrough(ip: string, host: string): Promise<{ mode?: unknown; owner?
83
83
  });
84
84
  }
85
85
 
86
- export async function daemonAnswersAt(url: string, owner: string | null): Promise<boolean> {
86
+ async function daemonAnswersAt(url: string, owner: string | null): Promise<boolean> {
87
87
  const host = new URL(url).host;
88
88
  const addresses = await publicAddresses(host);
89
89
  if (addresses.length === 0) return false;
@@ -162,7 +162,7 @@ export type Resolves = (host: string) => Promise<boolean>;
162
162
  const RESOLVE_EVERY_MS = 3_000;
163
163
  const RESOLVE_GIVE_UP_MS = 180_000;
164
164
 
165
- export async function resolvesPublicly(host: string): Promise<boolean> {
165
+ async function resolvesPublicly(host: string): Promise<boolean> {
166
166
  return (await publicAddresses(host)).length > 0;
167
167
  }
168
168
 
@@ -5,7 +5,7 @@ import type { ToolGroup } from '@metro-labs/core/stations/types';
5
5
 
6
6
  export type Access = 'allow' | 'ask' | 'deny';
7
7
 
8
- export const ACCESS: readonly Access[] = ['allow', 'ask', 'deny'];
8
+ const ACCESS: readonly Access[] = ['allow', 'ask', 'deny'];
9
9
 
10
10
  export interface ToolPolicy {
11
11
  read?: Access;
@@ -165,7 +165,7 @@ function bundleApi(deps: LocalModeDeps): BundleApiDeps {
165
165
  }
166
166
 
167
167
  function localModeInfo(): ModeInfo {
168
- return { mode: 'local', owner: localOwner(), project: 'localdaemon', version: METRO_VERSION };
168
+ return { mode: 'local', owner: localOwner(), version: METRO_VERSION };
169
169
  }
170
170
 
171
171
  async function accountCall(
@@ -195,9 +195,6 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
195
195
  claudeApi: {},
196
196
  updateApi: { restart: deps.restart },
197
197
  controlApi: { restart: deps.restart, stop: deps.stop },
198
- schedulesApi: true,
199
- filesApi: true,
200
- vaultApi: true,
201
198
  ownerApi: { setOwner: (owner) => setLocalOwner(owner) },
202
199
  machineApi: {},
203
200
  modelApi: {},
@@ -20,16 +20,12 @@ import { handleTerminalRequest, type TerminalApiDeps } from '../terminal/api.js'
20
20
  import { handleSchedulesRequest } from '../agent-user/schedules-api.js';
21
21
  import { handleFilesRequest } from '../agent-user/files-api.js';
22
22
  import { handleVaultRequest } from '../vault/api.js';
23
- import { handleMetroUserRequest } from '../metro-user/api.js';
24
23
 
25
24
  export interface SessionApis {
26
25
  agentApi?: AgentApiDeps;
27
26
  bundleApi?: BundleApiDeps;
28
27
  updateApi?: UpdateApiDeps;
29
28
  controlApi?: ControlApiDeps;
30
- schedulesApi?: true;
31
- filesApi?: true;
32
- vaultApi?: true;
33
29
  ownerApi?: OwnerApiDeps;
34
30
  machineApi?: MachineApiDeps;
35
31
  modelApi?: ModelApiDeps;
@@ -56,10 +52,9 @@ export function handleSessionApis(
56
52
  ...when(apis.bundleApi, (d) => handleBundleRequest(req, res, d)),
57
53
  ...when(apis.updateApi, (d) => handleUpdateRequest(req, res, d)),
58
54
  ...when(apis.controlApi, (d) => handleControlRequest(req, res, d)),
59
- ...when(apis.schedulesApi, () => handleSchedulesRequest(req, res)),
60
- ...when(apis.filesApi, () => handleFilesRequest(req, res)),
61
- ...when(apis.vaultApi, () => handleVaultRequest(req, res)),
62
- ...when(apis.vaultApi, () => handleMetroUserRequest(req, res)),
55
+ () => handleSchedulesRequest(req, res),
56
+ () => handleFilesRequest(req, res),
57
+ () => handleVaultRequest(req, res),
63
58
  ...when(apis.ownerApi, (d) => handleOwnerRequest(req, res, d)),
64
59
  ...when(apis.machineApi, (d) => handleMachineRequest(req, res, d)),
65
60
  ...when(apis.modelApi, (d) => handleModelRequest(req, res, d)),
@@ -17,13 +17,13 @@ export interface MachineApiDeps {
17
17
 
18
18
  const bootedAt = new Date(Date.now() - process.uptime() * 1000).toISOString();
19
19
 
20
- export interface Disk {
20
+ interface Disk {
21
21
  path: string;
22
22
  totalBytes: number;
23
23
  freeBytes: number;
24
24
  }
25
25
 
26
- export async function diskInfo(path = homedir()): Promise<Disk | null> {
26
+ async function diskInfo(path = homedir()): Promise<Disk | null> {
27
27
  try {
28
28
  const s = await statfs(path);
29
29
  return { path, totalBytes: s.bsize * s.blocks, freeBytes: s.bsize * s.bavail };
@@ -13,13 +13,13 @@ const UPDATE_TIMEOUT_MS = 5 * 60_000;
13
13
  const RESTART_DELAY_MS = 500;
14
14
  const OUTPUT_TAIL = 800;
15
15
 
16
- export interface UpdateCheck {
16
+ interface UpdateCheck {
17
17
  current: string;
18
18
  latest: string;
19
19
  newer: boolean;
20
20
  }
21
21
 
22
- export interface UpdateResult {
22
+ interface UpdateResult {
23
23
  updated: boolean;
24
24
  version: string;
25
25
  restarting: boolean;
@@ -50,13 +50,13 @@ export interface AttachView {
50
50
  expiresAt: number;
51
51
  }
52
52
 
53
- export type CompleteAttach = (
53
+ type CompleteAttach = (
54
54
  owner: AttachOwner,
55
55
  station: StationName,
56
56
  config: Record<string, unknown>,
57
57
  ) => Promise<{ accountId: string; activated: boolean }>;
58
58
 
59
- export type AuthorizeAttach = (owner: AttachOwner) => Promise<void>;
59
+ type AuthorizeAttach = (owner: AttachOwner) => Promise<void>;
60
60
 
61
61
  export interface AttachSessionDeps {
62
62
  authorize: AuthorizeAttach;
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { stringOf } from '@metro-labs/http/api-http';
2
3
  import { rmSync } from 'node:fs';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { errMsg, log } from '@metro-labs/core/log';
@@ -43,8 +44,6 @@ interface VerifyReply {
43
44
  error?: unknown;
44
45
  }
45
46
 
46
- const str = (value: unknown): string => (typeof value === 'string' ? value : '');
47
-
48
47
  function readReply(raw: string): VerifyReply {
49
48
  const line = raw.trim().split('\n').at(-1) ?? '';
50
49
  try {
@@ -97,13 +96,13 @@ export async function verifyXmtpKeyOutOfProcess(
97
96
  const reply = readReply(raw);
98
97
  if (reply.ok !== true)
99
98
  throw new XmtpAttachError(
100
- withoutKey(str(reply.error) || UNREACHABLE, privateKey),
99
+ withoutKey(stringOf(reply.error) || UNREACHABLE, privateKey),
101
100
  );
102
101
  const identity = {
103
- inboxId: str(reply.inboxId),
104
- address: str(reply.address),
105
- installationId: str(reply.installationId),
106
- dbPath: str(reply.dbPath),
102
+ inboxId: stringOf(reply.inboxId),
103
+ address: stringOf(reply.address),
104
+ installationId: stringOf(reply.installationId),
105
+ dbPath: stringOf(reply.dbPath),
107
106
  };
108
107
  if (identity.inboxId === '' || identity.address === '')
109
108
  throw new XmtpAttachError(UNREACHABLE);
@@ -6,7 +6,7 @@ import type { Pending } from '@metro-labs/core/trains/protocol';
6
6
  export const RESTART_BACKOFFS_MS = [1_000, 5_000, 30_000] as const;
7
7
  export const MAX_CONSECUTIVE_FAILS = 5;
8
8
  export const HOT_RELOAD_DEBOUNCE_MS = 300;
9
- export const TRAIN_EXT = /\.(ts|js|mjs)$/;
9
+ const TRAIN_EXT = /\.(ts|js|mjs)$/;
10
10
 
11
11
  export interface TrainState {
12
12
  name: string;
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, statSync, type FSWatcher } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { errMsg, log } from '@metro-labs/core/log';
4
- import { daemonSelf } from '@metro-labs/core/events';
4
+ import { userSelf } from '@metro-labs/core/events';
5
5
  import {
6
6
  drainLines,
7
7
  failAllPending,
@@ -209,7 +209,7 @@ export class TrainSupervisor {
209
209
  ...process.env,
210
210
  ...DYLD_FALLBACK_ENV,
211
211
  METRO_TRAIN_NAME: state.name,
212
- METRO_SELF_URI: daemonSelf(),
212
+ METRO_SELF_URI: userSelf(),
213
213
  },
214
214
  });
215
215
  state.proc = proc;
@@ -56,7 +56,7 @@ function tmuxAvailable(deps: TerminalApiDeps): boolean {
56
56
  return run.error === undefined && run.status === 0;
57
57
  }
58
58
 
59
- export function tmuxSessions(): string[] {
59
+ function tmuxSessions(): string[] {
60
60
  const run = spawnSync(...asAgent('tmux', ['list-sessions', '-F', '#{session_name}']), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
61
61
  if (run.error !== undefined || run.status !== 0) return [];
62
62
  return run.stdout
@@ -65,7 +65,7 @@ export function tmuxSessions(): string[] {
65
65
  .filter((l) => SESSION_RE.test(l));
66
66
  }
67
67
 
68
- export function sessionOf(raw: unknown): string {
68
+ function sessionOf(raw: unknown): string {
69
69
  if (raw === undefined || raw === null || raw === '')
70
70
  throw new ApiError('name the tmux session to open; GET /api/terminal lists the ones that exist', 400);
71
71
  if (typeof raw !== 'string' || !SESSION_RE.test(raw))
@@ -45,7 +45,7 @@ function sizeTmuxWindow(command: string[], session: string, cols: number, rows:
45
45
  const dimension = (raw: unknown): number | null =>
46
46
  typeof raw === 'number' && Number.isInteger(raw) && raw > 1 && raw <= MAX_DIMENSION ? raw : null;
47
47
 
48
- export function sizeFrom(control: unknown): { cols: number; rows: number } | null {
48
+ function sizeFrom(control: unknown): { cols: number; rows: number } | null {
49
49
  if (!isRecord(control)) return null;
50
50
  const cols = dimension(control.cols);
51
51
  const rows = dimension(control.rows);
@@ -1,48 +1,9 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { mustHelper, runningAsMetro } from '../metro-user/privilege.js';
1
+ import { mustHelper } from '../metro-user/privilege.js';
3
2
 
4
- const CHAIN = 'METRO_AGENT';
5
- const TOOLS = ['iptables', 'ip6tables'] as const;
6
-
7
- export interface FirewallRunner {
8
- run: (file: string, args: string[]) => number | null;
9
- }
10
-
11
- const realRunner: FirewallRunner = { run: (file, args) => spawnSync(file, ['-w', ...args], { stdio: 'ignore' }).status };
12
-
13
- export const chainRules = (): string[][] => [
14
- ['-A', CHAIN, '-o', 'lo', '-j', 'ACCEPT'],
15
- ['-A', CHAIN, '-m', 'conntrack', '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT'],
16
- ['-A', CHAIN, '-p', 'udp', '--dport', '53', '-j', 'ACCEPT'],
17
- ['-A', CHAIN, '-p', 'tcp', '--dport', '53', '-j', 'ACCEPT'],
18
- ['-A', CHAIN, '-p', 'tcp', '-j', 'REJECT', '--reject-with', 'tcp-reset'],
19
- ['-A', CHAIN, '-j', 'REJECT'],
20
- ];
21
-
22
- const jump = (uid: number): string[] => ['OUTPUT', '-m', 'owner', '--uid-owner', String(uid), '-j', CHAIN];
23
-
24
- export function applyFirewall(uid: number, runner: FirewallRunner = realRunner): void {
25
- if (runner === realRunner && runningAsMetro()) {
26
- mustHelper(['firewall-on']);
27
- return;
28
- }
29
- for (const tool of TOOLS) {
30
- runner.run(tool, ['-N', CHAIN]);
31
- if (runner.run(tool, ['-F', CHAIN]) !== 0) throw new Error(`${tool} refused to prepare the ${CHAIN} chain`);
32
- for (const rule of chainRules()) if (runner.run(tool, rule) !== 0) throw new Error(`${tool} refused a rule: ${rule.join(' ')}`);
33
- if (runner.run(tool, ['-C', ...jump(uid)]) !== 0 && runner.run(tool, ['-I', ...jump(uid)]) !== 0)
34
- throw new Error(`${tool} refused to send the agent's traffic to ${CHAIN}`);
35
- }
3
+ export function applyFirewall(): void {
4
+ mustHelper(['firewall-on']);
36
5
  }
37
6
 
38
- export function removeFirewall(uid: number, runner: FirewallRunner = realRunner): void {
39
- if (runner === realRunner && runningAsMetro()) {
40
- mustHelper(['firewall-off']);
41
- return;
42
- }
43
- for (const tool of TOOLS) {
44
- for (let i = 0; i < 10 && runner.run(tool, ['-C', ...jump(uid)]) === 0; i += 1) runner.run(tool, ['-D', ...jump(uid)]);
45
- runner.run(tool, ['-F', CHAIN]);
46
- runner.run(tool, ['-X', CHAIN]);
47
- }
7
+ export function removeFirewall(): void {
8
+ mustHelper(['firewall-off']);
48
9
  }