@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.
- package/dist/claude.js +4 -15
- package/dist/cli.js +5 -3
- package/dist/hold.js +1 -2
- package/dist/runtime-install.js +4 -5
- package/dist/runtime.js +1 -4
- package/dist/serve.js +4 -5
- package/dist/service.js +82 -21
- package/dist/update.js +1 -1
- package/package.json +2 -2
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/core/src/events.ts +4 -2
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +12 -25
- package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +45 -22
- package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +0 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +8 -80
- package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +3 -5
- package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +4 -9
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +9 -39
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +9 -28
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +8 -116
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +11 -35
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +7 -9
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +3 -29
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +5 -8
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +5 -8
- package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +3 -6
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +25 -3
- package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +2 -3
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +6 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +7 -6
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +7 -7
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -4
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +2 -4
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +19 -22
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +6 -8
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +3 -6
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +2 -3
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +28 -1
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +4 -4
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +6 -6
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +7 -7
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +10 -31
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +3 -8
- package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +6 -7
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +5 -44
- package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +4 -4
- package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +6 -7
- package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +12 -13
- package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +5 -22
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +1 -19
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +7 -35
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +5 -10
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +1 -1
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +1 -2
- package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +1 -1
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +11 -12
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +2 -5
- package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +0 -1
- package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +1 -15
- package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +0 -6
- package/runtime/node_modules/@metro-labs/outlook/src/format.ts +0 -1
- package/runtime/node_modules/@metro-labs/outlook/src/state.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +4 -22
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +2 -2
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +1 -3
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +1 -2
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +4 -37
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +7 -23
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +1 -4
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +4 -9
- package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +3 -3
- package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -11
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +2 -12
- package/runtime/node_modules/@metro-labs/threema/src/files.ts +4 -15
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +2 -7
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +1 -2
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +5 -26
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +5 -6
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -3
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -12
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +14 -5
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +31 -78
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -4
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +1 -6
- package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +2 -2
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
- package/runtime/runtime.json +1 -1
- package/runtime/node_modules/@metro-labs/core/src/identity.ts +0 -7
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +0 -12
- package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +0 -13
- package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +0 -29
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +0 -43
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +0 -165
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -1
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +0 -22
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -1
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -1
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
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
|
-
|
|
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 '
|
|
4
|
+
import { stringOf } from '@metro-labs/http/api-http';
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
interface EffectivePolicy {
|
|
64
63
|
read: Access;
|
|
65
64
|
write: Access;
|
|
66
65
|
tools: Record<string, Access>;
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 <
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
+
const HELPER_VERSION = 1;
|
|
3
3
|
export const SUDOERS_PATH = '/etc/sudoers.d/metro';
|
|
4
4
|
export const METRO_USER = 'metro';
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
20
|
+
interface Disk {
|
|
21
21
|
path: string;
|
|
22
22
|
totalBytes: number;
|
|
23
23
|
freeBytes: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
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
|
-
|
|
16
|
+
interface UpdateCheck {
|
|
17
17
|
current: string;
|
|
18
18
|
latest: string;
|
|
19
19
|
newer: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
99
|
+
withoutKey(stringOf(reply.error) || UNREACHABLE, privateKey),
|
|
101
100
|
);
|
|
102
101
|
const identity = {
|
|
103
|
-
inboxId:
|
|
104
|
-
address:
|
|
105
|
-
installationId:
|
|
106
|
-
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
|
-
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import { mustHelper, runningAsMetro } from '../metro-user/privilege.js';
|
|
1
|
+
import { mustHelper } from '../metro-user/privilege.js';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
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(
|
|
39
|
-
|
|
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
|
}
|