@stage-labs/metro 0.1.0-beta.3 → 0.1.0-beta.30
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/api.js +131 -0
- package/dist/bedrock-eventstream.js +120 -0
- package/dist/bedrock-proxy.js +260 -0
- package/dist/bedrock-upstream.js +72 -0
- package/dist/bedrock.js +104 -0
- package/dist/claude.js +15 -0
- package/dist/cli.js +157 -253
- package/dist/control.js +158 -0
- package/dist/plugin.js +82 -0
- package/dist/prompt.js +61 -0
- package/dist/runtime.js +94 -0
- package/dist/store.js +70 -0
- package/dist/tail.js +120 -0
- package/dist/update.js +33 -0
- package/dist/version.js +97 -0
- package/package.json +34 -21
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +337 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +321 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +123 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +339 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +359 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +302 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +130 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +308 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +382 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
- package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
- package/runtime/runtime.json +3 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -114
- package/dist/agents/claude.js +0 -229
- package/dist/agents/codex.js +0 -282
- package/dist/agents/types.js +0 -3
- package/dist/channels/discord.js +0 -142
- package/dist/channels/telegram.js +0 -209
- package/dist/lib/dotenv.js +0 -31
- package/dist/lib/scope-cache.js +0 -85
- package/dist/lib/streaming.js +0 -207
- package/dist/log.js +0 -12
- package/dist/orchestrator.js +0 -399
- package/dist/paths.js +0 -33
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { errMsg, log } from './log.js';
|
|
2
|
+
import type { LoadedAgent, StationSource } from '../db/materialize.js';
|
|
3
|
+
|
|
4
|
+
const POLL_MS = 30_000;
|
|
5
|
+
const BACKOFF_MS = 5 * 60_000;
|
|
6
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
7
|
+
|
|
8
|
+
export class RuntimeRevoked extends Error {}
|
|
9
|
+
|
|
10
|
+
const LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
11
|
+
|
|
12
|
+
export function carriesSecretsSafely(base: string): boolean {
|
|
13
|
+
let url: URL;
|
|
14
|
+
try {
|
|
15
|
+
url = new URL(base);
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (url.protocol === 'https:') return true;
|
|
20
|
+
return url.protocol === 'http:' && LOOPBACK.has(url.hostname);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RuntimeConfig {
|
|
24
|
+
url: string;
|
|
25
|
+
token: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function runtimeConfigFromEnv(): RuntimeConfig | null {
|
|
29
|
+
const token = process.env.METRO_RUN_TOKEN?.trim() ?? '';
|
|
30
|
+
if (token === '') return null;
|
|
31
|
+
const raw = (process.env.METRO_URL?.trim() ?? '').replace(/\/+$/, '');
|
|
32
|
+
const url = raw === '' ? 'https://mcp.metro.box' : raw;
|
|
33
|
+
if (!carriesSecretsSafely(url))
|
|
34
|
+
throw new Error(
|
|
35
|
+
`refusing to fetch station credentials from ${url} in the clear — ` +
|
|
36
|
+
'use https, or a loopback address',
|
|
37
|
+
);
|
|
38
|
+
return { url, token };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function httpSource(cfg: RuntimeConfig): StationSource {
|
|
42
|
+
return async () => {
|
|
43
|
+
const res = await fetch(`${cfg.url}/api/run/config`, {
|
|
44
|
+
headers: { authorization: `Bearer ${cfg.token}` },
|
|
45
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
46
|
+
redirect: 'manual',
|
|
47
|
+
});
|
|
48
|
+
if (res.status === 401 || res.status === 403 || res.status === 409)
|
|
49
|
+
throw new RuntimeRevoked(
|
|
50
|
+
`metro no longer recognises this machine as the holder (${String(res.status)})`,
|
|
51
|
+
);
|
|
52
|
+
if (!res.ok)
|
|
53
|
+
throw new Error(`metro answered ${String(res.status)} for the run config`);
|
|
54
|
+
const body = (await res.json()) as { agent?: LoadedAgent };
|
|
55
|
+
if (!body.agent) throw new Error('metro returned no agent');
|
|
56
|
+
return [body.agent];
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PollerDeps {
|
|
61
|
+
sync: () => Promise<void>;
|
|
62
|
+
stopAll: () => Promise<void>;
|
|
63
|
+
everyMs?: number;
|
|
64
|
+
backoffMs?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function startRuntimePoller(deps: PollerDeps): () => void {
|
|
68
|
+
const everyMs = deps.everyMs ?? POLL_MS;
|
|
69
|
+
const backoffMs = deps.backoffMs ?? BACKOFF_MS;
|
|
70
|
+
let stopped = false;
|
|
71
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
72
|
+
|
|
73
|
+
const arm = (ms: number): void => {
|
|
74
|
+
if (stopped) return;
|
|
75
|
+
timer = setTimeout(() => {
|
|
76
|
+
tick().catch((err: unknown) => {
|
|
77
|
+
log.error({ err: errMsg(err) }, 'runtime: poller failed');
|
|
78
|
+
});
|
|
79
|
+
}, ms);
|
|
80
|
+
timer.unref?.();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
async function tick(): Promise<void> {
|
|
84
|
+
if (stopped) return;
|
|
85
|
+
try {
|
|
86
|
+
await deps.sync();
|
|
87
|
+
arm(everyMs);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err instanceof RuntimeRevoked) {
|
|
90
|
+
stopped = true;
|
|
91
|
+
log.error({ reason: err.message }, 'runtime: revoked — stopping stations');
|
|
92
|
+
await deps.stopAll();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
log.warn({ err: errMsg(err) }, 'runtime: could not reach metro; retrying');
|
|
96
|
+
arm(backoffMs);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
arm(everyMs);
|
|
101
|
+
return () => {
|
|
102
|
+
stopped = true;
|
|
103
|
+
if (timer !== null) clearTimeout(timer);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { dirname } from 'node:path';
|
|
11
|
+
import { errMsg, log } from './log.js';
|
|
12
|
+
|
|
13
|
+
export function ensureSecureDir(dir: string): void {
|
|
14
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
15
|
+
try {
|
|
16
|
+
chmodSync(dir, 0o700);
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function chmodIfExists(path: string, mode = 0o600): void {
|
|
22
|
+
if (!existsSync(path)) return;
|
|
23
|
+
try {
|
|
24
|
+
if ((statSync(path).mode & 0o777) !== mode) chmodSync(path, mode);
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeSecure(path: string, data: string): void {
|
|
30
|
+
ensureSecureDir(dirname(path));
|
|
31
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
32
|
+
writeFileSync(tmp, data, { mode: 0o600 });
|
|
33
|
+
try {
|
|
34
|
+
chmodSync(tmp, 0o600);
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
renameSync(tmp, path);
|
|
38
|
+
chmodIfExists(path, 0o600);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readJson<T>(
|
|
42
|
+
path: string,
|
|
43
|
+
fallback: T,
|
|
44
|
+
opts?: { warn?: string },
|
|
45
|
+
): T {
|
|
46
|
+
if (!existsSync(path)) return fallback;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(readFileSync(path, 'utf8')) as T;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (opts?.warn) log.warn({ err: errMsg(err), path }, opts.warn);
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writeJson(path: string, value: unknown): void {
|
|
56
|
+
writeFileSync(path, JSON.stringify(value, null, 2));
|
|
57
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { apiSession, cors, sendJson } from './api-http.js';
|
|
3
|
+
|
|
4
|
+
const PATH = '/api/session';
|
|
5
|
+
|
|
6
|
+
export function handleSessionApiRequest(
|
|
7
|
+
req: IncomingMessage,
|
|
8
|
+
res: ServerResponse,
|
|
9
|
+
): boolean {
|
|
10
|
+
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
11
|
+
if (path !== PATH) return false;
|
|
12
|
+
if (req.method === 'OPTIONS') {
|
|
13
|
+
res.writeHead(204, cors(req)).end();
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (req.method !== 'GET') {
|
|
17
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const session = apiSession(req);
|
|
21
|
+
if (!session) {
|
|
22
|
+
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
sendJson(req, res, 200, { email: session.email });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { handleSessionApiRequest } from './session-api.js';
|
|
3
|
+
import { handleAgentApiRequest, type AgentApiDeps } from './agent-api.js';
|
|
4
|
+
import {
|
|
5
|
+
handleConnectorApiRequest,
|
|
6
|
+
type ConnectorApiDeps,
|
|
7
|
+
} from './connector-api.js';
|
|
8
|
+
import { handleCollectionApiRequest } from './collection-api.js';
|
|
9
|
+
import { handleCliPairRequest } from './cli-pair-api.js';
|
|
10
|
+
import { handleRunApiRequest, type RunApiDeps } from './run-api.js';
|
|
11
|
+
import type { RelayApiDeps } from './relay.js';
|
|
12
|
+
import {
|
|
13
|
+
handleProjectApiRequest,
|
|
14
|
+
type ProjectApiDeps,
|
|
15
|
+
} from './project-api.js';
|
|
16
|
+
|
|
17
|
+
export interface SessionApis {
|
|
18
|
+
agentApi?: AgentApiDeps;
|
|
19
|
+
connectorApi?: ConnectorApiDeps;
|
|
20
|
+
projectApi?: ProjectApiDeps;
|
|
21
|
+
runApi?: RunApiDeps;
|
|
22
|
+
relayApi?: RelayApiDeps;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function handleSessionApis(
|
|
26
|
+
req: IncomingMessage,
|
|
27
|
+
res: ServerResponse,
|
|
28
|
+
apis: SessionApis,
|
|
29
|
+
): boolean {
|
|
30
|
+
const { agentApi, connectorApi, projectApi, runApi } = apis;
|
|
31
|
+
const routes: (() => boolean)[] = [() => handleSessionApiRequest(req, res)];
|
|
32
|
+
if (runApi) routes.push(() => handleRunApiRequest(req, res, runApi));
|
|
33
|
+
if (projectApi)
|
|
34
|
+
routes.push(() => handleProjectApiRequest(req, res, projectApi));
|
|
35
|
+
if (connectorApi)
|
|
36
|
+
routes.push(
|
|
37
|
+
() => handleCliPairRequest(req, res, connectorApi),
|
|
38
|
+
() => handleCollectionApiRequest(req, res, connectorApi),
|
|
39
|
+
() => handleConnectorApiRequest(req, res, connectorApi),
|
|
40
|
+
);
|
|
41
|
+
if (agentApi) routes.push(() => handleAgentApiRequest(req, res, agentApi));
|
|
42
|
+
return routes.some((run) => run());
|
|
43
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { ID_RE } from '../db/ids.js';
|
|
3
|
+
|
|
4
|
+
export class SessionError extends Error {}
|
|
5
|
+
|
|
6
|
+
const b64url = (buf: Buffer): string =>
|
|
7
|
+
buf
|
|
8
|
+
.toString('base64')
|
|
9
|
+
.replace(/\+/g, '-')
|
|
10
|
+
.replace(/\//g, '_')
|
|
11
|
+
.replace(/=+$/, '');
|
|
12
|
+
|
|
13
|
+
const b64urlJson = (v: unknown): string =>
|
|
14
|
+
b64url(Buffer.from(JSON.stringify(v)));
|
|
15
|
+
|
|
16
|
+
const decodeJson = (seg: string): unknown =>
|
|
17
|
+
JSON.parse(
|
|
18
|
+
Buffer.from(seg.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString(
|
|
19
|
+
'utf8',
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const HEADER = b64urlJson({ alg: 'HS256', typ: 'JWT' });
|
|
24
|
+
|
|
25
|
+
function sign(payload: Record<string, unknown>, secret: string): string {
|
|
26
|
+
if (secret === '') throw new SessionError('missing signing secret');
|
|
27
|
+
const body = b64urlJson(payload);
|
|
28
|
+
const data = `${HEADER}.${body}`;
|
|
29
|
+
const sig = b64url(createHmac('sha256', secret).update(data).digest());
|
|
30
|
+
return `${data}.${sig}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function verify(token: string, secret: string): Record<string, unknown> {
|
|
34
|
+
if (secret === '') throw new SessionError('missing signing secret');
|
|
35
|
+
const parts = token.split('.');
|
|
36
|
+
if (parts.length !== 3) throw new SessionError('malformed token');
|
|
37
|
+
const data = `${parts[0]}.${parts[1]}`;
|
|
38
|
+
const expected = b64url(createHmac('sha256', secret).update(data).digest());
|
|
39
|
+
const a = Buffer.from(parts[2] ?? '');
|
|
40
|
+
const b = Buffer.from(expected);
|
|
41
|
+
if (a.length !== b.length || !timingSafeEqual(a, b))
|
|
42
|
+
throw new SessionError('bad signature');
|
|
43
|
+
let payload: unknown;
|
|
44
|
+
try {
|
|
45
|
+
payload = decodeJson(parts[1] ?? '');
|
|
46
|
+
} catch {
|
|
47
|
+
throw new SessionError('malformed payload');
|
|
48
|
+
}
|
|
49
|
+
if (typeof payload !== 'object' || payload === null)
|
|
50
|
+
throw new SessionError('malformed payload');
|
|
51
|
+
return payload as Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const nowSec = (now?: number): number => Math.floor((now ?? Date.now()) / 1000);
|
|
55
|
+
|
|
56
|
+
export function newNonce(): string {
|
|
57
|
+
return b64url(randomBytes(16));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface StateClaims {
|
|
61
|
+
return_to: string;
|
|
62
|
+
nonce: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function signState(
|
|
66
|
+
claims: StateClaims,
|
|
67
|
+
secret: string,
|
|
68
|
+
opts: { ttlSec?: number; now?: number } = {},
|
|
69
|
+
): string {
|
|
70
|
+
const iat = nowSec(opts.now);
|
|
71
|
+
return sign(
|
|
72
|
+
{
|
|
73
|
+
typ: 'state',
|
|
74
|
+
return_to: claims.return_to,
|
|
75
|
+
nonce: claims.nonce,
|
|
76
|
+
iat,
|
|
77
|
+
exp: iat + (opts.ttlSec ?? 600),
|
|
78
|
+
},
|
|
79
|
+
secret,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function verifyState(
|
|
84
|
+
token: string,
|
|
85
|
+
secret: string,
|
|
86
|
+
now?: number,
|
|
87
|
+
): StateClaims {
|
|
88
|
+
const p = verify(token, secret);
|
|
89
|
+
if (p.typ !== 'state') throw new SessionError('wrong token type');
|
|
90
|
+
if (typeof p.exp !== 'number' || p.exp < nowSec(now))
|
|
91
|
+
throw new SessionError('state expired');
|
|
92
|
+
if (typeof p.return_to !== 'string' || typeof p.nonce !== 'string')
|
|
93
|
+
throw new SessionError('malformed state');
|
|
94
|
+
return { return_to: p.return_to, nonce: p.nonce };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface SessionClaims {
|
|
98
|
+
email: string;
|
|
99
|
+
agentIds: string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface CliClaims {
|
|
103
|
+
email: string;
|
|
104
|
+
collectionId: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface RunClaims {
|
|
108
|
+
email: string;
|
|
109
|
+
agentId: string;
|
|
110
|
+
runtimeId: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function signSession(
|
|
114
|
+
claims: SessionClaims,
|
|
115
|
+
secret: string,
|
|
116
|
+
opts: { ttlSec?: number; now?: number } = {},
|
|
117
|
+
): string {
|
|
118
|
+
const iat = nowSec(opts.now);
|
|
119
|
+
return sign(
|
|
120
|
+
{
|
|
121
|
+
typ: 'session',
|
|
122
|
+
sub: claims.email,
|
|
123
|
+
agent_ids: claims.agentIds,
|
|
124
|
+
iat,
|
|
125
|
+
exp: iat + (opts.ttlSec ?? 30 * 24 * 3600),
|
|
126
|
+
},
|
|
127
|
+
secret,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function verifySession(
|
|
132
|
+
token: string,
|
|
133
|
+
secret: string,
|
|
134
|
+
now?: number,
|
|
135
|
+
): SessionClaims {
|
|
136
|
+
const p = verify(token, secret);
|
|
137
|
+
if (p.typ !== 'session') throw new SessionError('wrong token type');
|
|
138
|
+
if (typeof p.exp !== 'number' || p.exp < nowSec(now))
|
|
139
|
+
throw new SessionError('session expired');
|
|
140
|
+
const ids = p.agent_ids;
|
|
141
|
+
if (
|
|
142
|
+
typeof p.sub !== 'string' ||
|
|
143
|
+
!Array.isArray(ids) ||
|
|
144
|
+
ids.some((a) => typeof a !== 'string' || !ID_RE.test(a))
|
|
145
|
+
)
|
|
146
|
+
throw new SessionError('malformed session');
|
|
147
|
+
return { email: p.sub, agentIds: ids as string[] };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function signCliToken(
|
|
151
|
+
claims: CliClaims,
|
|
152
|
+
secret: string,
|
|
153
|
+
opts: { ttlSec?: number; now?: number } = {},
|
|
154
|
+
): string {
|
|
155
|
+
const iat = nowSec(opts.now);
|
|
156
|
+
return sign(
|
|
157
|
+
{
|
|
158
|
+
typ: 'cli',
|
|
159
|
+
sub: claims.email,
|
|
160
|
+
collection: claims.collectionId,
|
|
161
|
+
iat,
|
|
162
|
+
exp: iat + (opts.ttlSec ?? 30 * 24 * 3600),
|
|
163
|
+
},
|
|
164
|
+
secret,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function verifyCliToken(
|
|
169
|
+
token: string,
|
|
170
|
+
secret: string,
|
|
171
|
+
now?: number,
|
|
172
|
+
): CliClaims {
|
|
173
|
+
const p = verify(token, secret);
|
|
174
|
+
if (p.typ !== 'cli') throw new SessionError('wrong token type');
|
|
175
|
+
if (typeof p.exp !== 'number' || p.exp < nowSec(now))
|
|
176
|
+
throw new SessionError('cli token expired');
|
|
177
|
+
if (typeof p.sub !== 'string' || typeof p.collection !== 'string')
|
|
178
|
+
throw new SessionError('malformed cli token');
|
|
179
|
+
return { email: p.sub, collectionId: p.collection };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function signRunToken(
|
|
183
|
+
claims: RunClaims,
|
|
184
|
+
secret: string,
|
|
185
|
+
opts: { ttlSec?: number; now?: number } = {},
|
|
186
|
+
): string {
|
|
187
|
+
const iat = nowSec(opts.now);
|
|
188
|
+
return sign(
|
|
189
|
+
{
|
|
190
|
+
typ: 'run',
|
|
191
|
+
sub: claims.email,
|
|
192
|
+
agent: claims.agentId,
|
|
193
|
+
rt: claims.runtimeId,
|
|
194
|
+
iat,
|
|
195
|
+
exp: iat + (opts.ttlSec ?? 365 * 24 * 3600),
|
|
196
|
+
},
|
|
197
|
+
secret,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function verifyRunToken(
|
|
202
|
+
token: string,
|
|
203
|
+
secret: string,
|
|
204
|
+
now?: number,
|
|
205
|
+
): RunClaims {
|
|
206
|
+
const p = verify(token, secret);
|
|
207
|
+
if (p.typ !== 'run') throw new SessionError('wrong token type');
|
|
208
|
+
if (typeof p.exp !== 'number' || p.exp < nowSec(now))
|
|
209
|
+
throw new SessionError('run token expired');
|
|
210
|
+
if (
|
|
211
|
+
typeof p.sub !== 'string' ||
|
|
212
|
+
typeof p.agent !== 'string' ||
|
|
213
|
+
typeof p.rt !== 'string' ||
|
|
214
|
+
!ID_RE.test(p.agent)
|
|
215
|
+
)
|
|
216
|
+
throw new SessionError('malformed run token');
|
|
217
|
+
return { email: p.sub, agentId: p.agent, runtimeId: p.rt };
|
|
218
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from 'node:fs';
|
|
2
|
+
import { join, parse as parsePath } from 'node:path';
|
|
3
|
+
import { errMsg, log } from './log.js';
|
|
4
|
+
import type { Pending } from './protocol.js';
|
|
5
|
+
|
|
6
|
+
export const RESTART_BACKOFFS_MS = [1_000, 5_000, 30_000] as const;
|
|
7
|
+
export const MAX_CONSECUTIVE_FAILS = 5;
|
|
8
|
+
export const HOT_RELOAD_DEBOUNCE_MS = 300;
|
|
9
|
+
export const TRAIN_EXT = /\.(ts|js|mjs)$/;
|
|
10
|
+
|
|
11
|
+
export interface TrainState {
|
|
12
|
+
name: string;
|
|
13
|
+
path: string;
|
|
14
|
+
proc: ReturnType<typeof Bun.spawn> | null;
|
|
15
|
+
pending: Map<string, Pending>;
|
|
16
|
+
buf: string;
|
|
17
|
+
errBuf: string;
|
|
18
|
+
failCount: number;
|
|
19
|
+
restartTimer: ReturnType<typeof setTimeout> | null;
|
|
20
|
+
startedAt: string | null;
|
|
21
|
+
stopped: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function startWatcher(
|
|
25
|
+
dir: string,
|
|
26
|
+
timers: Map<string, ReturnType<typeof setTimeout>>,
|
|
27
|
+
onChange: (name: string, path: string) => void,
|
|
28
|
+
): FSWatcher | null {
|
|
29
|
+
try {
|
|
30
|
+
const w = watch(dir, (_event, filename) => {
|
|
31
|
+
if (!filename) return;
|
|
32
|
+
const base = filename;
|
|
33
|
+
if (!TRAIN_EXT.test(base) || base.startsWith('_') || base.startsWith('.'))
|
|
34
|
+
return;
|
|
35
|
+
const name = parsePath(base).name;
|
|
36
|
+
const existing = timers.get(name);
|
|
37
|
+
if (existing) clearTimeout(existing);
|
|
38
|
+
timers.set(
|
|
39
|
+
name,
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
timers.delete(name);
|
|
42
|
+
onChange(name, join(dir, base));
|
|
43
|
+
}, HOT_RELOAD_DEBOUNCE_MS),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
w.on('error', (err) => {
|
|
47
|
+
log.warn({ err: errMsg(err) }, 'train hot-reload: watcher error');
|
|
48
|
+
});
|
|
49
|
+
log.info({ dir }, 'train hot-reload: watching');
|
|
50
|
+
return w;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
log.warn(
|
|
53
|
+
{ err: errMsg(err) },
|
|
54
|
+
'train hot-reload: watch unavailable (on-demand restart only)',
|
|
55
|
+
);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function killGracefully(
|
|
61
|
+
proc: ReturnType<typeof Bun.spawn> | null,
|
|
62
|
+
): Promise<unknown> | null {
|
|
63
|
+
if (proc?.exitCode !== null) return null;
|
|
64
|
+
try {
|
|
65
|
+
proc.kill('SIGTERM');
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
const grace = setTimeout(() => {
|
|
69
|
+
try {
|
|
70
|
+
proc.kill('SIGKILL');
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}, 2_000);
|
|
74
|
+
return proc.exited.finally(() => {
|
|
75
|
+
clearTimeout(grace);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function pumpStream(
|
|
80
|
+
stream: unknown,
|
|
81
|
+
name: string,
|
|
82
|
+
kind: 'stdout' | 'stderr',
|
|
83
|
+
onChunk: (s: string) => void,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
if (!stream || typeof stream === 'number') return;
|
|
86
|
+
const reader = (stream as ReadableStream<Uint8Array>).getReader();
|
|
87
|
+
const dec = new TextDecoder();
|
|
88
|
+
try {
|
|
89
|
+
while (true) {
|
|
90
|
+
const { value, done } = await reader.read();
|
|
91
|
+
if (done) break;
|
|
92
|
+
onChunk(dec.decode(value, { stream: true }));
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
log.debug({ name, err: errMsg(err) }, `train: ${kind} pump ended`);
|
|
96
|
+
}
|
|
97
|
+
}
|