@themoltnet/agent-daemon 0.47.0 → 0.49.0

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/cli.js CHANGED
@@ -6,15 +6,15 @@ import "multiformats/cid";
6
6
  import "multiformats/codecs/json";
7
7
  import "multiformats/hashes/sha2";
8
8
  import "typebox/value";
9
- import { dirname, join, resolve } from "node:path";
9
+ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
10
10
  import { parseArgs, promisify } from "node:util";
11
11
  import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, createLocalSeedSigner, resolveAgentIdentity, resolveProfileWarmSessionTtlSec, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
12
- import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets } from "@themoltnet/pi-runtime";
13
- import { connect, createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
14
- import { execFile, execFileSync } from "node:child_process";
15
- import { createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, realpathSync, rmSync } from "node:fs";
16
- import { AuthenticationError, MoltNetError, createExecutorAttestor, readConfig, resolveEnvSecretReference, resolveIdentitySeed } from "@themoltnet/sdk";
17
- import { createHash, randomUUID } from "node:crypto";
12
+ import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveRuntimeProfileModel } from "@themoltnet/pi-runtime";
13
+ import { FILE_SECRET_PROVIDER, FileSecretProvider, connect, createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
14
+ import { execFile, execFileSync, spawn } from "node:child_process";
15
+ import { constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
16
+ import { AuthenticationError, MoltNetError, agentKeyKey, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, identitySeedKey, parseSecretReferenceString, readConfig, register, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, resolveOAuth2ClientSecret } from "@themoltnet/sdk";
17
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
18
18
  import { once } from "node:events";
19
19
  import { pino, transport } from "pino";
20
20
  import { metrics } from "@opentelemetry/api";
@@ -26,10 +26,19 @@ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
26
26
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
27
27
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
28
28
  import { AsyncLocalStorage } from "node:async_hooks";
29
- import { getModel } from "@earendil-works/pi-ai";
30
- import { mkdir, realpath, stat } from "node:fs/promises";
29
+ import { mkdir, open, realpath, stat } from "node:fs/promises";
31
30
  import { pipeline } from "node:stream/promises";
32
- import { Writable } from "node:stream";
31
+ import cors from "@fastify/cors";
32
+ import helmet from "@fastify/helmet";
33
+ import { getOAuthProviders } from "@earendil-works/pi-ai/oauth";
34
+ import { AuthStorage } from "@earendil-works/pi-coding-agent";
35
+ import { Transform, Writable } from "node:stream";
36
+ import { writePiConfig } from "@themoltnet/pi-runtime/pi-config";
37
+ import { homedir } from "node:os";
38
+ import { lock } from "proper-lockfile";
39
+ import { StringDecoder } from "node:string_decoder";
40
+ import rateLimit from "@fastify/rate-limit";
41
+ import Fastify from "fastify";
33
42
  import { createGzip } from "node:zlib";
34
43
  //#region ../../libs/tasks/src/rubric.ts
35
44
  /**
@@ -3417,6 +3426,9 @@ Commands:
3417
3426
  once Claim and execute one specific queued task by id, then exit.
3418
3427
  drain Poll until the queue has nothing claimable, then exit.
3419
3428
  Useful for batch eval runs and demos.
3429
+ serve Loopback supervisor for console-managed runs: pairing,
3430
+ agent/provider config store, and start/stop of poll/drain
3431
+ child processes. Binds 127.0.0.1 only.
3420
3432
  sync-sessions
3421
3433
  Repair durable runtime-session checkpoints from local slot files.
3422
3434
 
@@ -3549,6 +3561,39 @@ Example:
3549
3561
  function isHelpFlag(args) {
3550
3562
  return args.includes("--help") || args.includes("-h");
3551
3563
  }
3564
+ var SERVE_HELP = `\
3565
+ agent-daemon serve — loopback supervisor for console-managed runs.
3566
+
3567
+ Binds 127.0.0.1 only. A paired Console origin configures agents and
3568
+ providers (secret references only) and starts/stops poll/drain runs as
3569
+ child processes of this supervisor.
3570
+
3571
+ Options:
3572
+ --port <n> Loopback port. Default: 17374.
3573
+ Env: MOLTNET_SERVE_PORT.
3574
+ --allowed-origins <csv> Exact Console origins allowed to pair.
3575
+ Default: https://console.themolt.net.
3576
+ Env: MOLTNET_SERVE_ALLOWED_ORIGINS.
3577
+ --root <path> Config root. Default: ~/.config/moltnet
3578
+ (or MOLTNET_SERVE_ROOT).
3579
+ --api-url <url> Default MoltNet API for new managed agents.
3580
+ Default: https://api.themolt.net.
3581
+ `;
3582
+ //#endregion
3583
+ //#region src/lib/identity-pin.ts
3584
+ /** Compare every pinned field without choosing a caller-specific error type. */
3585
+ function assessIdentityPin(current, expected) {
3586
+ for (const [field, label] of [
3587
+ ["identityId", "identity id"],
3588
+ ["publicKey", "public key"],
3589
+ ["fingerprint", "fingerprint"]
3590
+ ]) if (!current[field] || current[field] !== expected[field]) return {
3591
+ ok: false,
3592
+ field,
3593
+ label
3594
+ };
3595
+ return { ok: true };
3596
+ }
3552
3597
  //#endregion
3553
3598
  //#region src/lib/agent-context.ts
3554
3599
  /**
@@ -3612,6 +3657,8 @@ async function validateStartupBinding(options) {
3612
3657
  }
3613
3658
  const assessment = assessStartupBinding(whoami, options.teamId);
3614
3659
  if (!assessment.ok) throw new Error(`Daemon startup validation failed: ${assessment.reason}`);
3660
+ const expected = options.expectedIdentity;
3661
+ if (expected && !assessIdentityPin(whoami, expected).ok) throw new Error("Daemon startup validation failed: authenticated identity does not match the serve activation.");
3615
3662
  return whoami;
3616
3663
  }
3617
3664
  /**
@@ -3690,6 +3737,7 @@ function resolveCredentialRoots(agentRootDir) {
3690
3737
  function loadConfig() {
3691
3738
  assertSingleCredentialForm("MOLTNET_AGENT_KEY", "MOLTNET_AGENT_KEY_REF");
3692
3739
  assertSingleCredentialForm("MOLTNET_PRIVATE_KEY", "MOLTNET_PRIVATE_KEY_REF");
3740
+ const expectedIdentity = readExpectedIdentity();
3693
3741
  return {
3694
3742
  otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
3695
3743
  logLevel: process.env["LOG_LEVEL"] ?? "",
@@ -3703,7 +3751,25 @@ function loadConfig() {
3703
3751
  profileCredentialRequirements: process.env["MOLTNET_PROFILE_CREDENTIAL_REQUIREMENTS"] ?? "",
3704
3752
  credentialBindings: process.env["MOLTNET_CREDENTIAL_BINDINGS"] ?? "",
3705
3753
  credentialEnforcement: process.env["MOLTNET_CREDENTIAL_ENFORCEMENT"] ?? "",
3706
- traceIdlePolling: readBoolean("MOLTNET_TRACE_IDLE_POLLING", process.env["MOLTNET_TRACE_IDLE_POLLING"])
3754
+ traceIdlePolling: readBoolean("MOLTNET_TRACE_IDLE_POLLING", process.env["MOLTNET_TRACE_IDLE_POLLING"]),
3755
+ ...expectedIdentity ? { expectedIdentity } : {}
3756
+ };
3757
+ }
3758
+ function readExpectedIdentity() {
3759
+ const identityId = process.env["MOLTNET_EXPECTED_IDENTITY_ID"]?.trim() ?? "";
3760
+ const publicKey = process.env["MOLTNET_EXPECTED_PUBLIC_KEY"]?.trim() ?? "";
3761
+ const fingerprint = process.env["MOLTNET_EXPECTED_FINGERPRINT"]?.trim() ?? "";
3762
+ const present = [
3763
+ identityId,
3764
+ publicKey,
3765
+ fingerprint
3766
+ ].filter(Boolean).length;
3767
+ if (present === 0) return void 0;
3768
+ if (present !== 3) throw new Error("MOLTNET_EXPECTED_IDENTITY_ID, MOLTNET_EXPECTED_PUBLIC_KEY, and MOLTNET_EXPECTED_FINGERPRINT must be set together");
3769
+ return {
3770
+ identityId,
3771
+ publicKey,
3772
+ fingerprint
3707
3773
  };
3708
3774
  }
3709
3775
  function assertSingleCredentialForm(valueName, refName) {
@@ -3718,6 +3784,20 @@ function readBoolean(name, value) {
3718
3784
  function activatePiCodingAgentDir(path) {
3719
3785
  process.env["PI_CODING_AGENT_DIR"] = path;
3720
3786
  }
3787
+ function loadServeEnvConfig() {
3788
+ return {
3789
+ port: process.env["MOLTNET_SERVE_PORT"] ?? "",
3790
+ allowedOrigins: process.env["MOLTNET_SERVE_ALLOWED_ORIGINS"] ?? "",
3791
+ root: process.env["MOLTNET_SERVE_ROOT"] ?? "",
3792
+ xdgConfigHome: process.env["XDG_CONFIG_HOME"] ?? "",
3793
+ apiUrl: process.env["MOLTNET_API_URL"] ?? "",
3794
+ logLevel: process.env["LOG_LEVEL"] ?? ""
3795
+ };
3796
+ }
3797
+ /** Full process environment for spawned serve run children. */
3798
+ function processEnvSnapshot() {
3799
+ return process.env;
3800
+ }
3721
3801
  //#endregion
3722
3802
  //#region src/lib/abort-active-attempt.ts
3723
3803
  /** Best-effort signal cleanup; lease expiry remains the final backstop. */
@@ -4346,13 +4426,11 @@ function attestPreparedRuntime(prepared, signingPrivateKey) {
4346
4426
  //#endregion
4347
4427
  //#region src/lib/retry-triage.ts
4348
4428
  var RETRYABLE_CODES = new Set([
4349
- "checkpoint_upload_failed",
4350
4429
  "complete_call_failed",
4351
4430
  "daemon_abort",
4352
4431
  "dispatch_expired",
4353
4432
  "lease_expired",
4354
4433
  "llm_api_error",
4355
- "runtime_session_checkpoint_failed",
4356
4434
  "session_prompt_failed"
4357
4435
  ]);
4358
4436
  var NON_RETRYABLE_CODES = new Set([
@@ -4402,7 +4480,7 @@ var NON_RETRYABLE_MESSAGE_PATTERNS = [
4402
4480
  async function classifyAttemptFailure(input) {
4403
4481
  const deterministic = classifyDeterministically(input.error);
4404
4482
  if (input.remainingAttempts !== null && input.remainingAttempts !== void 0 && input.remainingAttempts <= 0) {
4405
- const deterministicReason = deterministic === "ambiguous" ? "" : ` Deterministic policy classified the failure as ${deterministic}.`;
4483
+ const deterministicReason = deterministic === "ambiguous" ? "" : deterministic === "retryable" ? " The failure type is retryable, but no attempts remain." : " The failure type is non-retryable.";
4406
4484
  return {
4407
4485
  error: withRetryInfo(input.error, {
4408
4486
  retryable: false,
@@ -4468,6 +4546,7 @@ async function classifyAttemptFailure(input) {
4468
4546
  function classifyDeterministically(error) {
4469
4547
  const code = error.code.toLowerCase();
4470
4548
  const message = error.message;
4549
+ if (code === "runtime_session_upload_failed") return error.retryable === true ? "retryable" : "non_retryable";
4471
4550
  if (NON_RETRYABLE_CODES.has(code)) return "non_retryable";
4472
4551
  if (NON_RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) return "non_retryable";
4473
4552
  if (RETRYABLE_CODES.has(code)) return "retryable";
@@ -5170,13 +5249,17 @@ function runWithDaemonRuntimeContext(context, callback) {
5170
5249
  //#endregion
5171
5250
  //#region src/lib/runtime-profile-retry-triage.ts
5172
5251
  function createRuntimeProfileRetryTriage(options) {
5173
- return createPiRetryTriage({
5174
- model: getModel(options.runtimeProfile.provider, options.runtimeProfile.model),
5175
- thinkingLevel: options.runtimeProfile.thinkingLevel,
5176
- piAgentDir: options.piAgentDir,
5177
- timeoutMs: options.timeoutMs,
5178
- cwd: options.cwd
5179
- });
5252
+ return async (input) => {
5253
+ const { modelHandle, modelRuntime } = await resolveRuntimeProfileModel(options.piAgentDir, options.runtimeProfile.provider, options.runtimeProfile.model);
5254
+ return createPiRetryTriage({
5255
+ model: modelHandle,
5256
+ modelRuntime,
5257
+ thinkingLevel: options.runtimeProfile.thinkingLevel,
5258
+ piAgentDir: options.piAgentDir,
5259
+ timeoutMs: options.timeoutMs,
5260
+ cwd: options.cwd
5261
+ })(input);
5262
+ };
5180
5263
  }
5181
5264
  //#endregion
5182
5265
  //#region src/lib/runtime-resource-reaper.ts
@@ -5457,15 +5540,29 @@ function applyRuntimeSessionUploadFailure(output, err) {
5457
5540
  error: {
5458
5541
  code: "runtime_session_upload_failed",
5459
5542
  message: "Task completed, but durable runtime session checkpoint upload failed: " + (err instanceof Error ? err.message : String(err)),
5460
- retryable: true
5543
+ retryable: isTransientUploadError(err)
5461
5544
  },
5462
5545
  output: null,
5463
5546
  outputCid: null,
5464
5547
  status: "failed"
5465
5548
  };
5466
5549
  }
5550
+ /**
5551
+ * Transient faults worth retrying in-attempt: network-level errors
5552
+ * (no HTTP status at all) and 5xx/429 responses. A 4xx (auth,
5553
+ * validation, not-found) will not heal on retry.
5554
+ */
5555
+ function isTransientUploadError(error) {
5556
+ if (!(error instanceof Error)) return false;
5557
+ const statusCode = error.statusCode;
5558
+ if (typeof statusCode !== "number") return true;
5559
+ return statusCode >= 500 || statusCode === 429;
5560
+ }
5561
+ var defaultSleep = (ms) => new Promise((resolve) => {
5562
+ setTimeout(resolve, ms);
5563
+ });
5467
5564
  function createApiRuntimeSessionStore(args) {
5468
- const { agent } = args;
5565
+ const { agent, logger, uploadRetry } = args;
5469
5566
  return {
5470
5567
  async findRuntimeSessionByTaskAttempt(teamId, taskId, attemptN) {
5471
5568
  return agent.runtimeSessions.getForAttempt({
@@ -5486,18 +5583,40 @@ function createApiRuntimeSessionStore(args) {
5486
5583
  async uploadAttemptFinal(input) {
5487
5584
  const sessionPath = resolveLatestPiSessionPath(input.sessionDir);
5488
5585
  if (!sessionPath) throw new Error(`Cannot upload runtime session for ${input.taskId}/${input.attemptN}: no local session file in ${input.sessionDir}`);
5489
- await agent.runtimeSessions.upload({
5490
- attemptN: input.attemptN,
5491
- taskId: input.taskId
5492
- }, createReadStream(sessionPath), {
5493
- parentSessionId: input.parentSessionId ?? void 0,
5494
- sessionKind: input.sessionKind,
5495
- sourceRuntimeProfileId: input.sourceRuntimeProfileId ?? void 0,
5496
- sourceSlotId: input.sourceSlotId ?? void 0
5497
- }, { teamId: input.teamId });
5586
+ const maxTries = uploadRetry?.maxTries ?? 3;
5587
+ const baseDelayMs = uploadRetry?.baseDelayMs ?? 750;
5588
+ const sleep = uploadRetry?.sleep ?? defaultSleep;
5589
+ for (let tryN = 1;; tryN += 1) try {
5590
+ await agent.runtimeSessions.upload({
5591
+ attemptN: input.attemptN,
5592
+ taskId: input.taskId
5593
+ }, createReadStream(sessionPath), {
5594
+ parentSessionId: input.parentSessionId ?? void 0,
5595
+ sessionKind: input.sessionKind,
5596
+ sourceRuntimeProfileId: input.sourceRuntimeProfileId ?? void 0,
5597
+ sourceSlotId: input.sourceSlotId ?? void 0
5598
+ }, { teamId: input.teamId });
5599
+ return;
5600
+ } catch (error) {
5601
+ if (tryN >= maxTries || !isTransientUploadError(error)) throw error;
5602
+ const delayMs = baseDelayMs * tryN;
5603
+ logger?.warn({
5604
+ event: "agent-daemon.runtime_session_upload_retry",
5605
+ attemptN: input.attemptN,
5606
+ delayMs,
5607
+ statusCode: uploadStatusCode(error),
5608
+ taskId: input.taskId,
5609
+ tryN
5610
+ }, "Retrying durable runtime session upload");
5611
+ await sleep(delayMs);
5612
+ }
5498
5613
  }
5499
5614
  };
5500
5615
  }
5616
+ function uploadStatusCode(error) {
5617
+ const statusCode = error?.statusCode;
5618
+ return typeof statusCode === "number" ? statusCode : void 0;
5619
+ }
5501
5620
  function resolveContinueFrom(claimedTask) {
5502
5621
  return claimedTask.task.input.continueFrom;
5503
5622
  }
@@ -5790,7 +5909,8 @@ async function runPolling(opts) {
5790
5909
  gate = "authenticate_and_bind";
5791
5910
  const whoami = await validateStartupBinding({
5792
5911
  agent: resolvedContext.agent,
5793
- teamId
5912
+ teamId,
5913
+ expectedIdentity: cfg.expectedIdentity
5794
5914
  });
5795
5915
  gate = "resolve_signing_material";
5796
5916
  const privateKey = await resolveExecutorSigningPrivateKey({
@@ -5873,7 +5993,10 @@ async function runPolling(opts) {
5873
5993
  throw new Error(`No safe runtime profiles remain. ${details}`);
5874
5994
  }
5875
5995
  const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
5876
- const runtimeSessionStore = createApiRuntimeSessionStore({ agent: ctx.agent });
5996
+ const runtimeSessionStore = createApiRuntimeSessionStore({
5997
+ agent: ctx.agent,
5998
+ logger: { warn: (context, message) => rootLogger.warn(context, message) }
5999
+ });
5877
6000
  const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
5878
6001
  const runtimeInstanceId = createRuntimeInstanceId();
5879
6002
  const runtimes = /* @__PURE__ */ new Map();
@@ -6443,7 +6566,8 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6443
6566
  gate = "authenticate_and_bind";
6444
6567
  const whoami = await validateStartupBinding({
6445
6568
  agent: resolvedContext.agent,
6446
- teamId: values.team
6569
+ teamId: values.team,
6570
+ expectedIdentity: cfg.expectedIdentity
6447
6571
  });
6448
6572
  gate = "resolve_signing_material";
6449
6573
  const privateKey = await resolveExecutorSigningPrivateKey({
@@ -6523,7 +6647,10 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6523
6647
  activatePiCodingAgentDir(piAgentDir.path);
6524
6648
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
6525
6649
  const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
6526
- const runtimeSessionStore = createApiRuntimeSessionStore({ agent: ctx.agent });
6650
+ const runtimeSessionStore = createApiRuntimeSessionStore({
6651
+ agent: ctx.agent,
6652
+ logger: { warn: (context, message) => rootLogger.warn(context, message) }
6653
+ });
6527
6654
  const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
6528
6655
  const runtimeInstanceId = createRuntimeInstanceId();
6529
6656
  const slotIdentity = {
@@ -6854,6 +6981,2650 @@ function runPoll(argv, runtimeAdapter) {
6854
6981
  });
6855
6982
  }
6856
6983
  //#endregion
6984
+ //#region ../../libs/loopback-companion/src/errors.ts
6985
+ var LoopbackViolationError = class extends Error {
6986
+ name = "LoopbackViolationError";
6987
+ constructor(kind, message, options) {
6988
+ super(message, options);
6989
+ this.kind = kind;
6990
+ }
6991
+ };
6992
+ function isLoopbackViolation(error) {
6993
+ return error instanceof LoopbackViolationError;
6994
+ }
6995
+ //#endregion
6996
+ //#region ../../libs/loopback-companion/src/origin.ts
6997
+ /** Hostnames accepted as loopback for companion servers and origins. */
6998
+ function isLoopbackHostname(hostname) {
6999
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
7000
+ }
7001
+ /**
7002
+ * Normalize and validate a browser origin. Accepts exact `https:` origins,
7003
+ * or `http:` origins whose host is loopback. Rejects values that carry a
7004
+ * path, trailing slash, credentials, or any other non-origin decoration
7005
+ * (`url.origin !== value` catches all of those).
7006
+ */
7007
+ function normalizeOrigin(value) {
7008
+ let url;
7009
+ try {
7010
+ url = new URL(value);
7011
+ } catch (cause) {
7012
+ throw new LoopbackViolationError("origin_invalid", "Origin is not valid", { cause });
7013
+ }
7014
+ if (url.origin !== value || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHostname(url.hostname))) throw new LoopbackViolationError("origin_invalid", "Origin is not valid");
7015
+ return url.origin;
7016
+ }
7017
+ /** Parse a comma-separated origin list (config format shared by companions). */
7018
+ function parseAllowedOrigins(csv) {
7019
+ return csv.split(",").map((origin) => origin.trim()).filter(Boolean);
7020
+ }
7021
+ /**
7022
+ * Exact-origin allowlist. Every configured origin is normalized eagerly so a
7023
+ * misconfigured allowlist fails at startup, not at request time.
7024
+ */
7025
+ var OriginAllowlist = class {
7026
+ origins;
7027
+ constructor(allowedOrigins) {
7028
+ if (allowedOrigins.length === 0) throw new Error("OriginAllowlist requires at least one origin");
7029
+ this.origins = new Set(allowedOrigins.map((origin) => normalizeOrigin(origin)));
7030
+ }
7031
+ has(origin) {
7032
+ try {
7033
+ return this.origins.has(normalizeOrigin(origin));
7034
+ } catch {
7035
+ return false;
7036
+ }
7037
+ }
7038
+ /** Return the normalized origin or throw `origin_not_allowed`. */
7039
+ assert(value) {
7040
+ let origin;
7041
+ try {
7042
+ origin = normalizeOrigin(value);
7043
+ } catch {
7044
+ throw new LoopbackViolationError("origin_not_allowed", "Origin is not allowed");
7045
+ }
7046
+ if (!this.origins.has(origin)) throw new LoopbackViolationError("origin_not_allowed", "Origin is not allowed");
7047
+ return origin;
7048
+ }
7049
+ };
7050
+ /** Extract and require the `Origin` header value. */
7051
+ function requireOriginHeader(headers) {
7052
+ const origin = headers.origin;
7053
+ if (typeof origin !== "string" || origin.length === 0) throw new LoopbackViolationError("origin_required", "Origin is required");
7054
+ return origin;
7055
+ }
7056
+ //#endregion
7057
+ //#region ../../libs/loopback-companion/src/fastify.ts
7058
+ var CORS_PREFLIGHT_MAX_AGE_SECONDS = 600;
7059
+ /**
7060
+ * Enforce that the `Host` header identifies loopback. Blocks DNS-rebinding
7061
+ * setups where a public hostname resolves to 127.0.0.1: the browser then
7062
+ * sends that hostname as `Host`, and the request is refused here even
7063
+ * though the socket is loopback.
7064
+ */
7065
+ function requireLoopbackHost(request) {
7066
+ const host = request.headers.host;
7067
+ if (!host) throw new LoopbackViolationError("host_required", "Host header is required");
7068
+ let hostname;
7069
+ try {
7070
+ hostname = new URL(`http://${host}`).hostname;
7071
+ } catch {
7072
+ throw new LoopbackViolationError("host_not_loopback", "Host header must identify loopback");
7073
+ }
7074
+ if (!isLoopbackHostname(hostname === "::1" ? "[::1]" : hostname)) throw new LoopbackViolationError("host_not_loopback", "Host header must identify loopback");
7075
+ }
7076
+ /**
7077
+ * Register the loopback-companion security profile on a Fastify app:
7078
+ *
7079
+ * - loopback `Host` enforcement on every request;
7080
+ * - `cache-control: no-store` on every response;
7081
+ * - strict UTF-8 JSON body parsing (invalid bodies raise a typed violation);
7082
+ * - exact-origin CORS (opaque/`null` origins get no CORS response but are
7083
+ * not rejected here — route-level controls stay mandatory);
7084
+ * - hardened helmet defaults.
7085
+ *
7086
+ */
7087
+ function registerLoopbackSecurity(app, options) {
7088
+ if (!options.allowedOrigins && !options.isOriginAllowed) throw new Error("registerLoopbackSecurity requires allowedOrigins or isOriginAllowed");
7089
+ const primaryAllowlist = options.allowedOrigins ? new OriginAllowlist(options.allowedOrigins) : null;
7090
+ const selfAllowlist = options.selfOrigins && options.selfOrigins.length > 0 ? new OriginAllowlist(options.selfOrigins) : null;
7091
+ const isOriginAllowed = (origin) => selfAllowlist?.has(origin) === true || primaryAllowlist?.has(origin) === true || options.isOriginAllowed?.(origin) === true;
7092
+ app.addHook("onRequest", (request, _reply, done) => {
7093
+ requireLoopbackHost(request);
7094
+ done();
7095
+ });
7096
+ app.addHook("onSend", async (_request, reply, payload) => {
7097
+ reply.header("cache-control", "no-store");
7098
+ return payload;
7099
+ });
7100
+ app.removeContentTypeParser("application/json");
7101
+ app.addContentTypeParser("application/json", { parseAs: "buffer" }, (_request, body, done) => {
7102
+ try {
7103
+ const json = new TextDecoder("utf-8", { fatal: true }).decode(typeof body === "string" ? Buffer.from(body) : body);
7104
+ done(null, JSON.parse(json));
7105
+ } catch (cause) {
7106
+ done(new LoopbackViolationError("body_not_utf8_json", "Request body must be valid UTF-8 JSON", { cause }), void 0);
7107
+ }
7108
+ });
7109
+ app.register(cors, {
7110
+ allowedHeaders: ["content-type", ...options.allowedHeaders ?? []],
7111
+ maxAge: CORS_PREFLIGHT_MAX_AGE_SECONDS,
7112
+ methods: [...options.methods ?? [
7113
+ "GET",
7114
+ "POST",
7115
+ "OPTIONS"
7116
+ ]],
7117
+ origin: (origin, callback) => {
7118
+ if (!origin || origin === "null") {
7119
+ callback(null, false);
7120
+ return;
7121
+ }
7122
+ if (isOriginAllowed(origin)) {
7123
+ callback(null, true);
7124
+ return;
7125
+ }
7126
+ callback(new LoopbackViolationError("origin_not_allowed", "Origin is not allowed"), false);
7127
+ }
7128
+ });
7129
+ app.register(helmet, {
7130
+ contentSecurityPolicy: {
7131
+ useDefaults: false,
7132
+ directives: options.contentSecurityPolicyDirectives ?? {
7133
+ baseUri: ["'none'"],
7134
+ defaultSrc: ["'none'"],
7135
+ formAction: ["'self'"],
7136
+ frameAncestors: ["'none'"],
7137
+ styleSrc: ["'unsafe-inline'"]
7138
+ }
7139
+ },
7140
+ crossOriginEmbedderPolicy: false,
7141
+ crossOriginOpenerPolicy: false,
7142
+ crossOriginResourcePolicy: { policy: "same-origin" },
7143
+ hsts: false,
7144
+ referrerPolicy: { policy: "no-referrer" }
7145
+ });
7146
+ }
7147
+ //#endregion
7148
+ //#region ../../libs/loopback-companion/src/fetch-metadata.ts
7149
+ function headerValue(headers, name) {
7150
+ const value = headers[name];
7151
+ return typeof value === "string" ? value : void 0;
7152
+ }
7153
+ /**
7154
+ * Require that a request is a top-level browser navigation (used by local
7155
+ * approval pages that must be opened as a document, never fetched).
7156
+ */
7157
+ function assertNavigationRequest(headers) {
7158
+ const site = headerValue(headers, "sec-fetch-site");
7159
+ const mode = headerValue(headers, "sec-fetch-mode");
7160
+ const destination = headerValue(headers, "sec-fetch-dest");
7161
+ if (site !== "cross-site" && site !== "same-origin" && site !== "none" || mode !== "navigate" || destination !== "document") throw new LoopbackViolationError("navigation_required", "Request must be opened as a browser navigation");
7162
+ }
7163
+ /**
7164
+ * Reject an explicit cross-site Fetch-Metadata signal. Only the explicit
7165
+ * `cross-site` value is rejected: Safari may omit Fetch Metadata on
7166
+ * same-origin form submissions, so the absence of the header is not treated
7167
+ * as a violation — callers keep their one-time token as the primary control.
7168
+ */
7169
+ function rejectExplicitCrossSite(headers) {
7170
+ if (headerValue(headers, "sec-fetch-site") === "cross-site") throw new LoopbackViolationError("cross_site_rejected", "Request must not originate cross-site");
7171
+ }
7172
+ //#endregion
7173
+ //#region src/lib/serve/pairing.ts
7174
+ /**
7175
+ * One-click pairing ceremony for the serve supervisor, mirroring the signer's
7176
+ * session/ceremony pattern (#2062 design):
7177
+ *
7178
+ * 1. Console (allowed origin) POSTs `/v1/pairings` → pending pairing bound
7179
+ * to that origin, with a one-time confirmation token.
7180
+ * 2. Console opens `http://127.0.0.1:<port>/pairings/<id>` in a new tab —
7181
+ * a navigation-gated local approval page naming the origin.
7182
+ * 3. One click POSTs the confirmation form (explicit cross-site rejected;
7183
+ * the one-time token is the primary CSRF control).
7184
+ * 4. Console claims `/v1/pairings/<id>/claim` from the same origin and
7185
+ * receives the bearer token exactly once; only its SHA-256 remains in
7186
+ * this supervisor process.
7187
+ *
7188
+ * The token exists for shared-machine cross-user protection and to bind
7189
+ * "this console session is the operator" — browser-vs-browser isolation is
7190
+ * already covered by the loopback-companion origin checks. Grants are
7191
+ * deliberately process-scoped: after the listening socket changes owners, a
7192
+ * token disclosed to an impostor on that port cannot authenticate to a later
7193
+ * supervisor process.
7194
+ */
7195
+ var PENDING_TTL_MS = 600 * 1e3;
7196
+ var ServePairingError = class extends Error {
7197
+ name = "ServePairingError";
7198
+ constructor(code, message) {
7199
+ super(message);
7200
+ this.code = code;
7201
+ }
7202
+ };
7203
+ function sha256Hex(value) {
7204
+ return createHash("sha256").update(value, "utf8").digest("hex");
7205
+ }
7206
+ function safeEqual(a, b) {
7207
+ const left = Buffer.from(a, "utf8");
7208
+ const right = Buffer.from(b, "utf8");
7209
+ return left.length === right.length && timingSafeEqual(left, right);
7210
+ }
7211
+ var PairingService = class {
7212
+ pending = /* @__PURE__ */ new Map();
7213
+ paired = /* @__PURE__ */ new Map();
7214
+ constructor(options = {}) {
7215
+ this.options = options;
7216
+ }
7217
+ now() {
7218
+ return this.options.now?.() ?? Date.now();
7219
+ }
7220
+ token() {
7221
+ return this.options.randomToken?.() ?? randomBytes(32).toString("base64url");
7222
+ }
7223
+ sweep() {
7224
+ const now = this.now();
7225
+ for (const [id, pairing] of this.pending) if (pairing.expiresAt <= now) this.pending.delete(id);
7226
+ }
7227
+ start(origin) {
7228
+ this.sweep();
7229
+ const pairingId = randomBytes(12).toString("hex");
7230
+ this.pending.set(pairingId, {
7231
+ origin,
7232
+ confirmToken: this.token(),
7233
+ expiresAt: this.now() + PENDING_TTL_MS,
7234
+ approved: false,
7235
+ bearerToken: null
7236
+ });
7237
+ return {
7238
+ pairingId,
7239
+ approvalPath: `/pairings/${pairingId}`
7240
+ };
7241
+ }
7242
+ /** Data for the local approval page. */
7243
+ approval(pairingId) {
7244
+ const pairing = this.require(pairingId);
7245
+ if (pairing.approved) throw new ServePairingError("pairing_invalid", "Pairing is already approved");
7246
+ return {
7247
+ origin: pairing.origin,
7248
+ confirmToken: pairing.confirmToken
7249
+ };
7250
+ }
7251
+ confirm(pairingId, confirmToken) {
7252
+ const pairing = this.require(pairingId);
7253
+ if (pairing.approved || !safeEqual(pairing.confirmToken, confirmToken)) throw new ServePairingError("pairing_invalid", "Confirmation token is not valid");
7254
+ pairing.approved = true;
7255
+ pairing.bearerToken = this.token();
7256
+ return { origin: pairing.origin };
7257
+ }
7258
+ claim(pairingId, origin) {
7259
+ const pairing = this.require(pairingId);
7260
+ if (pairing.origin !== origin) throw new ServePairingError("pairing_origin_mismatch", "Pairing belongs to a different origin");
7261
+ if (!pairing.approved || !pairing.bearerToken) throw new ServePairingError("pairing_not_approved", "Pairing has not been approved yet");
7262
+ const token = pairing.bearerToken;
7263
+ this.pending.delete(pairingId);
7264
+ this.paired.set(origin, sha256Hex(token));
7265
+ return { token };
7266
+ }
7267
+ verify(origin, token) {
7268
+ const tokenHash = this.paired.get(origin);
7269
+ if (!tokenHash || !safeEqual(tokenHash, sha256Hex(token))) throw new ServePairingError("pairing_token_invalid", "Pairing token is not valid for this origin");
7270
+ }
7271
+ require(pairingId) {
7272
+ this.sweep();
7273
+ const pairing = this.pending.get(pairingId);
7274
+ if (!pairing) throw new ServePairingError("pairing_not_found", "Pairing was not found or has expired");
7275
+ return pairing;
7276
+ }
7277
+ };
7278
+ function escapeHtml(value) {
7279
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
7280
+ }
7281
+ /** Minimal, dependency-free local approval page. */
7282
+ function renderPairingApprovalPage(input) {
7283
+ return `<!doctype html>
7284
+ <html lang="en">
7285
+ <head>
7286
+ <meta charset="utf-8" />
7287
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7288
+ <title>MoltNet Agent — approve connection</title>
7289
+ <style>
7290
+ :root { color-scheme: light dark; }
7291
+ body { margin: 0; font: 16px/1.5 system-ui, sans-serif; display: grid; place-items: center; min-height: 100vh; background: Canvas; color: CanvasText; }
7292
+ main { max-width: 26rem; padding: 2rem; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 12px; }
7293
+ h1 { font-size: 1.2rem; margin: 0 0 0.5rem; }
7294
+ code { font-size: 0.95em; word-break: break-all; }
7295
+ button { margin-top: 1.25rem; font: inherit; padding: 0.6rem 1.4rem; border-radius: 8px; border: 1px solid color-mix(in srgb, CanvasText 30%, transparent); cursor: pointer; }
7296
+ p.small { font-size: 0.85rem; opacity: 0.75; }
7297
+ </style>
7298
+ </head>
7299
+ <body>
7300
+ <main>
7301
+ <h1>Allow this site to manage local MoltNet agents?</h1>
7302
+ <p><code>${escapeHtml(input.origin)}</code> asks to configure agents and start or stop local daemon runs on this machine.</p>
7303
+ <p class="small">Approve only if you opened that page yourself. This grant lasts until the local supervisor stops.</p>
7304
+ <form method="post" action="/pairings/${escapeHtml(input.pairingId)}/confirm">
7305
+ <input type="hidden" name="confirmToken" value="${escapeHtml(input.confirmToken)}" />
7306
+ <button type="submit">Approve</button>
7307
+ </form>
7308
+ </main>
7309
+ </body>
7310
+ </html>
7311
+ `;
7312
+ }
7313
+ function renderPairingResultPage(input) {
7314
+ return `<!doctype html>
7315
+ <html lang="en">
7316
+ <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml(input.title)}</title>
7317
+ <style>:root{color-scheme:light dark}body{margin:0;font:16px/1.5 system-ui,sans-serif;display:grid;place-items:center;min-height:100vh;background:Canvas;color:CanvasText}main{max-width:26rem;padding:2rem}</style>
7318
+ </head>
7319
+ <body><main role="status"><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.message)}</p><p>You can close this tab.</p></main></body>
7320
+ </html>
7321
+ `;
7322
+ }
7323
+ //#endregion
7324
+ //#region src/lib/serve/provider-login.ts
7325
+ /**
7326
+ * Subscription-provider OAuth brokering for `serve` (#2061 slice 4).
7327
+ *
7328
+ * The console clicks "Connect"; serve runs the Pi OAuth flow host-side via
7329
+ * `AuthStorage.login()` (which owns persistence into the shared
7330
+ * `pi/auth.json` and token rotation thereafter). The browser only ever sees
7331
+ * the provider's authorize URL or device code — never tokens.
7332
+ *
7333
+ * Provider ids come from pi-ai's own OAuth registry (`getOAuthProviders`),
7334
+ * so serve stays in lockstep with what Pi can actually authenticate
7335
+ * (anthropic, openai-codex, github-copilot, …) without hardcoding.
7336
+ */
7337
+ var LOGIN_TTL_MS = 600 * 1e3;
7338
+ /** How long `start()` waits for the flow to surface a URL / device code. */
7339
+ var START_INFO_TIMEOUT_MS = 5e3;
7340
+ var ServeSubscriptionError = class extends Error {
7341
+ name = "ServeSubscriptionError";
7342
+ constructor(code, message) {
7343
+ super(message);
7344
+ this.code = code;
7345
+ }
7346
+ };
7347
+ var silentLogger = {
7348
+ info: () => void 0,
7349
+ warn: () => void 0,
7350
+ error: () => void 0
7351
+ };
7352
+ var ProviderLoginService = class {
7353
+ logins = /* @__PURE__ */ new Map();
7354
+ authStorage;
7355
+ logger;
7356
+ constructor(options) {
7357
+ this.options = options;
7358
+ this.authStorage = options.authStorage ?? AuthStorage.create(this.options.authPath);
7359
+ this.logger = options.logger ?? silentLogger;
7360
+ }
7361
+ now() {
7362
+ return this.options.now?.() ?? Date.now();
7363
+ }
7364
+ providers() {
7365
+ return this.options.listProviders?.() ?? getOAuthProviders().map((provider) => ({
7366
+ id: provider.id,
7367
+ name: provider.name
7368
+ }));
7369
+ }
7370
+ connected(providerId) {
7371
+ if (this.options.isConnected) return this.options.isConnected(providerId);
7372
+ try {
7373
+ return (this.options.authStorage ? this.authStorage : AuthStorage.create(this.options.authPath)).getAuthStatus(providerId).configured;
7374
+ } catch (error) {
7375
+ this.logger.warn({
7376
+ event: "serve.subscription_auth_read_failed",
7377
+ providerId,
7378
+ ...safeLoginError(error)
7379
+ }, "Could not read subscription authentication state");
7380
+ return false;
7381
+ }
7382
+ }
7383
+ list() {
7384
+ this.sweep();
7385
+ return this.providers().map((provider) => ({
7386
+ ...provider,
7387
+ connected: this.connected(provider.id)
7388
+ }));
7389
+ }
7390
+ sweep() {
7391
+ const now = this.now();
7392
+ for (const [id, login] of this.logins) if (login.startedAt + LOGIN_TTL_MS <= now) {
7393
+ this.invalidate(login, "expired");
7394
+ this.logins.delete(id);
7395
+ }
7396
+ }
7397
+ restoreCredential(login) {
7398
+ try {
7399
+ if (login.previousCredential) this.authStorage.set(login.providerId, login.previousCredential);
7400
+ else this.authStorage.logout(login.providerId);
7401
+ } catch (error) {
7402
+ this.logger.error({
7403
+ event: "serve.subscription_login_cleanup_failed",
7404
+ operationId: login.operationId,
7405
+ providerId: login.providerId,
7406
+ ...safeLoginError(error)
7407
+ }, "Could not restore subscription credentials after an invalidated login");
7408
+ }
7409
+ }
7410
+ invalidate(login, transition) {
7411
+ if (login.invalidated) return;
7412
+ login.invalidated = true;
7413
+ login.abort.abort(/* @__PURE__ */ new Error(`subscription login ${transition}`));
7414
+ this.restoreCredential(login);
7415
+ login.infoArrived();
7416
+ this.logger.info({
7417
+ event: "serve.subscription_login_transition",
7418
+ operationId: login.operationId,
7419
+ providerId: login.providerId,
7420
+ transition
7421
+ }, "Subscription login invalidated");
7422
+ }
7423
+ status(providerId) {
7424
+ this.sweep();
7425
+ const login = this.logins.get(providerId);
7426
+ if (!login) throw new ServeSubscriptionError("login_not_found", `no login in progress for "${providerId}"`);
7427
+ return snapshot(login);
7428
+ }
7429
+ /**
7430
+ * Start (or return the in-flight) login for a provider. Resolves once the
7431
+ * flow has surfaced an authorize URL / device code, completed, or the
7432
+ * start window elapsed — whichever comes first.
7433
+ */
7434
+ async start(providerId) {
7435
+ this.sweep();
7436
+ if (!this.providers().some((provider) => provider.id === providerId)) throw new ServeSubscriptionError("provider_unknown", `"${providerId}" is not a known subscription provider`);
7437
+ const existing = this.logins.get(providerId);
7438
+ if (existing && existing.status === "pending") return snapshot(existing);
7439
+ let infoArrived = () => void 0;
7440
+ const infoPromise = new Promise((resolvePromise) => {
7441
+ infoArrived = () => resolvePromise();
7442
+ });
7443
+ const abort = new AbortController();
7444
+ const login = {
7445
+ providerId,
7446
+ status: "pending",
7447
+ operationId: randomUUID(),
7448
+ startedAt: this.now(),
7449
+ infoArrived,
7450
+ abort,
7451
+ invalidated: false,
7452
+ previousCredential: this.authStorage.get(providerId)
7453
+ };
7454
+ this.logins.set(providerId, login);
7455
+ this.logger.info({
7456
+ event: "serve.subscription_login_transition",
7457
+ operationId: login.operationId,
7458
+ providerId,
7459
+ transition: "started"
7460
+ }, "Subscription login started");
7461
+ const callbacks = createLoginCallbacks(login, this.logger);
7462
+ (this.options.runLogin ?? ((id, loginCallbacks) => this.authStorage.login(id, loginCallbacks)))(providerId, callbacks).then(() => {
7463
+ if (login.invalidated) {
7464
+ this.restoreCredential(login);
7465
+ return;
7466
+ }
7467
+ if (!this.options.runLogin && !this.connected(providerId)) {
7468
+ login.status = "failed";
7469
+ login.error = "Subscription sign-in completed, but credentials were not persisted. Start again to retry.";
7470
+ this.logger.error({
7471
+ event: "serve.subscription_login_transition",
7472
+ operationId: login.operationId,
7473
+ providerId,
7474
+ transition: "persistence_failed"
7475
+ }, "Subscription login credentials were not persisted");
7476
+ } else {
7477
+ login.status = "completed";
7478
+ this.logger.info({
7479
+ event: "serve.subscription_login_transition",
7480
+ operationId: login.operationId,
7481
+ providerId,
7482
+ transition: "completed"
7483
+ }, "Subscription login completed");
7484
+ }
7485
+ login.infoArrived();
7486
+ }, (error) => {
7487
+ if (login.invalidated) {
7488
+ this.restoreCredential(login);
7489
+ return;
7490
+ }
7491
+ login.status = "failed";
7492
+ login.error = publicLoginError(error);
7493
+ this.logger.warn({
7494
+ event: "serve.subscription_login_transition",
7495
+ operationId: login.operationId,
7496
+ providerId,
7497
+ transition: "failed",
7498
+ ...safeLoginError(error)
7499
+ }, "Subscription login failed");
7500
+ login.infoArrived();
7501
+ });
7502
+ await Promise.race([infoPromise, new Promise((resolvePromise) => {
7503
+ setTimeout(resolvePromise, START_INFO_TIMEOUT_MS).unref?.();
7504
+ })]);
7505
+ return snapshot(login);
7506
+ }
7507
+ /** Abort an in-flight login and forget it. */
7508
+ cancel(providerId) {
7509
+ const login = this.logins.get(providerId);
7510
+ if (!login) throw new ServeSubscriptionError("login_not_found", `no login in progress for "${providerId}"`);
7511
+ this.invalidate(login, "cancelled");
7512
+ this.logins.delete(providerId);
7513
+ return {
7514
+ providerId,
7515
+ status: "cancelled"
7516
+ };
7517
+ }
7518
+ /** Abort every pending flow during supervisor shutdown. */
7519
+ close() {
7520
+ for (const login of this.logins.values()) this.invalidate(login, "shutdown");
7521
+ this.logins.clear();
7522
+ }
7523
+ };
7524
+ function createLoginCallbacks(login, logger) {
7525
+ return {
7526
+ onAuth: (info) => {
7527
+ login.authUrl = info.url;
7528
+ if (info.instructions) login.instructions = info.instructions;
7529
+ logger.info({
7530
+ event: "serve.subscription_login_transition",
7531
+ operationId: login.operationId,
7532
+ providerId: login.providerId,
7533
+ transition: "authorization_ready"
7534
+ }, "Subscription authorization URL ready");
7535
+ login.infoArrived();
7536
+ },
7537
+ onDeviceCode: (info) => {
7538
+ login.userCode = info.userCode;
7539
+ login.verificationUri = info.verificationUri;
7540
+ logger.info({
7541
+ event: "serve.subscription_login_transition",
7542
+ operationId: login.operationId,
7543
+ providerId: login.providerId,
7544
+ transition: "device_code_ready"
7545
+ }, "Subscription device code ready");
7546
+ login.infoArrived();
7547
+ },
7548
+ onPrompt: () => Promise.reject(new ServeSubscriptionError("login_unsupported_prompt", "This provider flow needs an interactive prompt; run `pi /login` in a terminal instead")),
7549
+ onSelect: (prompt) => Promise.resolve((prompt.options.find((option) => /device/i.test(option.id)) ?? prompt.options[0])?.id),
7550
+ signal: login.abort.signal
7551
+ };
7552
+ }
7553
+ function publicLoginError(error) {
7554
+ if (error instanceof ServeSubscriptionError) return error.message;
7555
+ return "Subscription sign-in failed. Start again to retry.";
7556
+ }
7557
+ function safeLoginError(error) {
7558
+ const result = { errorType: error instanceof Error ? error.name : typeof error };
7559
+ const code = error?.code;
7560
+ if (typeof code === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(code)) result["applicationCode"] = code;
7561
+ return result;
7562
+ }
7563
+ function snapshot(login) {
7564
+ const { providerId, status, authUrl, instructions, userCode, verificationUri, error } = login;
7565
+ return {
7566
+ providerId,
7567
+ status,
7568
+ ...authUrl ? { authUrl } : {},
7569
+ ...instructions ? { instructions } : {},
7570
+ ...userCode ? { userCode } : {},
7571
+ ...verificationUri ? { verificationUri } : {},
7572
+ ...error ? { error } : {}
7573
+ };
7574
+ }
7575
+ var NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
7576
+ var PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
7577
+ function assertStoreName(kind, value) {
7578
+ if (!NAME_RE.test(value)) throw new ServeStoreError("invalid_name", `${kind} must match ${NAME_RE.source}`);
7579
+ return value;
7580
+ }
7581
+ function assertProviderId(value) {
7582
+ if (!PROVIDER_ID_RE.test(value)) throw new ServeStoreError("invalid_name", `provider id must match ${PROVIDER_ID_RE.source}`);
7583
+ return value;
7584
+ }
7585
+ var ServeStoreError = class extends Error {
7586
+ name = "ServeStoreError";
7587
+ constructor(code, message, options) {
7588
+ super(message, options);
7589
+ this.code = code;
7590
+ }
7591
+ };
7592
+ /** `MOLTNET_SERVE_ROOT` override, else `$XDG_CONFIG_HOME/moltnet`, else `~/.config/moltnet`. */
7593
+ function resolveServeRoot(input) {
7594
+ const override = input.root?.trim();
7595
+ if (override) return override;
7596
+ const xdg = input.xdgConfigHome?.trim();
7597
+ return join(xdg || join(homedir(), ".config"), "moltnet");
7598
+ }
7599
+ function providerEnvName(providerId) {
7600
+ return `MOLTNET_PROVIDER_${assertProviderId(providerId).replaceAll("-", "_").toUpperCase()}_API_KEY`;
7601
+ }
7602
+ function assertProviderEnvName(providerId, value) {
7603
+ const expected = providerEnvName(providerId);
7604
+ if (value !== expected) throw new ServeStoreError("invalid_state", `provider envName must be ${expected}`);
7605
+ return value;
7606
+ }
7607
+ function readJson(path) {
7608
+ let raw;
7609
+ try {
7610
+ raw = readFileSync(path, "utf8");
7611
+ } catch (cause) {
7612
+ if (cause.code === "ENOENT") return null;
7613
+ throw new ServeStoreError("io_error", `could not read state at ${path}`, { cause });
7614
+ }
7615
+ try {
7616
+ return JSON.parse(raw);
7617
+ } catch (cause) {
7618
+ throw new ServeStoreError("invalid_state", `corrupt JSON at ${path}`, { cause });
7619
+ }
7620
+ }
7621
+ function writeJsonAtomic(path, value) {
7622
+ const temp = `${path}.${randomBytes(6).toString("hex")}.tmp`;
7623
+ try {
7624
+ writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
7625
+ renameSync(temp, path);
7626
+ } catch (cause) {
7627
+ try {
7628
+ rmSync(temp, { force: true });
7629
+ } catch {}
7630
+ throw cause;
7631
+ }
7632
+ }
7633
+ var ServeStore = class {
7634
+ root;
7635
+ agentsDir;
7636
+ runsDir;
7637
+ secretsDir;
7638
+ /** Shared Pi credential dir; `auth.json` inside is pi-managed (lockfiled). */
7639
+ piDir;
7640
+ constructor(root) {
7641
+ this.root = root;
7642
+ this.agentsDir = join(root, "agents");
7643
+ this.runsDir = join(root, "runs");
7644
+ this.secretsDir = join(root, "secrets");
7645
+ this.piDir = join(root, "pi");
7646
+ }
7647
+ get piAuthJsonPath() {
7648
+ return join(this.piDir, "auth.json");
7649
+ }
7650
+ /** Create the directory layout (0700) if missing. Idempotent. */
7651
+ ensure() {
7652
+ for (const dir of [
7653
+ this.root,
7654
+ this.agentsDir,
7655
+ this.runsDir,
7656
+ this.secretsDir
7657
+ ]) mkdirSync(dir, {
7658
+ recursive: true,
7659
+ mode: 448
7660
+ });
7661
+ return this;
7662
+ }
7663
+ get servePath() {
7664
+ return join(this.root, "serve.json");
7665
+ }
7666
+ readServeState() {
7667
+ const state = readJson(this.servePath);
7668
+ if (!state) return {
7669
+ version: 1,
7670
+ pendingRegistrations: {},
7671
+ activations: {}
7672
+ };
7673
+ if (!isRecord$1(state) || state.version !== 1) throw new ServeStoreError("invalid_state", `serve.json version ${String(isRecord$1(state) ? state.version : void 0)} is not supported`);
7674
+ if ("pairedOrigins" in state) throw new ServeStoreError("invalid_state", "serve.json uses the obsolete pairing format; clear the unreleased serve store and reconfigure it");
7675
+ if (!isRecord$1(state.pendingRegistrations) || !isRecord$1(state.activations)) throw new ServeStoreError("invalid_state", "serve.json is missing the version 1 activation map; clear the unreleased serve store and reconfigure it");
7676
+ for (const [alias, activation] of Object.entries(state.activations)) validateActivation(alias, activation);
7677
+ for (const [alias, registration] of Object.entries(state.pendingRegistrations)) {
7678
+ assertStoreName("agent name", alias);
7679
+ if (!isRecord$1(registration) || typeof registration.apiUrl !== "string" || registration.apiUrl.length === 0 || typeof registration.createdAt !== "string" || registration.createdAt.length === 0) throw new ServeStoreError("invalid_state", `pending registration "${alias}" is not valid`);
7680
+ }
7681
+ return {
7682
+ version: 1,
7683
+ pendingRegistrations: state.pendingRegistrations,
7684
+ activations: state.activations
7685
+ };
7686
+ }
7687
+ writeServeState(state) {
7688
+ writeJsonAtomic(this.servePath, state);
7689
+ }
7690
+ agentPath(name) {
7691
+ return storeChildPath(this.agentsDir, "agent name", name, ".json");
7692
+ }
7693
+ readAgentConfig(alias) {
7694
+ return readJson(this.agentPath(alias));
7695
+ }
7696
+ writeAgentConfig(alias, config) {
7697
+ writeJsonAtomic(this.agentPath(alias), config);
7698
+ }
7699
+ removeAgentConfig(alias) {
7700
+ rmSync(this.agentPath(alias), { force: true });
7701
+ }
7702
+ readActivation(alias) {
7703
+ return this.readServeState().activations[assertStoreName("agent name", alias)] ?? null;
7704
+ }
7705
+ hasPendingRegistration(alias) {
7706
+ return Boolean(this.readServeState().pendingRegistrations[assertStoreName("agent name", alias)]);
7707
+ }
7708
+ reserveRegistration(alias, apiUrl) {
7709
+ const name = assertStoreName("agent name", alias);
7710
+ const state = this.readServeState();
7711
+ if (state.activations[name] || state.pendingRegistrations[name]) throw new ServeStoreError("already_exists", `agent "${name}" already exists in the serve store`);
7712
+ state.pendingRegistrations[name] = {
7713
+ apiUrl,
7714
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
7715
+ };
7716
+ this.writeServeState(state);
7717
+ }
7718
+ clearPendingRegistration(alias) {
7719
+ const name = assertStoreName("agent name", alias);
7720
+ const state = this.readServeState();
7721
+ delete state.pendingRegistrations[name];
7722
+ this.writeServeState(state);
7723
+ }
7724
+ writeActivation(activation) {
7725
+ const alias = assertStoreName("agent name", activation.alias);
7726
+ validateActivation(alias, activation);
7727
+ const state = this.readServeState();
7728
+ state.activations[alias] = activation;
7729
+ if (activation.source === "managed") delete state.pendingRegistrations[alias];
7730
+ this.writeServeState(state);
7731
+ }
7732
+ listActivations() {
7733
+ return Object.values(this.readServeState().activations).sort((a, b) => a.alias.localeCompare(b.alias));
7734
+ }
7735
+ get providersPath() {
7736
+ return join(this.root, "providers.json");
7737
+ }
7738
+ readProviders() {
7739
+ const state = readJson(this.providersPath) ?? {};
7740
+ this.validateProviders(state);
7741
+ return state;
7742
+ }
7743
+ writeProviders(state) {
7744
+ this.validateProviders(state);
7745
+ writeJsonAtomic(this.providersPath, state);
7746
+ }
7747
+ validateProviders(state) {
7748
+ for (const [id, provider] of Object.entries(state)) {
7749
+ assertProviderId(id);
7750
+ assertProviderEnvName(id, provider.envName);
7751
+ }
7752
+ }
7753
+ runDir(id) {
7754
+ return storeChildPath(this.runsDir, "run id", id);
7755
+ }
7756
+ resolveRunLogPath(id) {
7757
+ let root;
7758
+ let runDir;
7759
+ try {
7760
+ root = realpathSync(this.runsDir);
7761
+ runDir = realpathSync(this.runDir(id));
7762
+ } catch (cause) {
7763
+ throw new ServeStoreError("io_error", "could not resolve run directory", { cause });
7764
+ }
7765
+ if (!isStrictDescendant(root, runDir)) throw new ServeStoreError("invalid_state", "run directory escapes its store");
7766
+ const logPath = join(runDir, "daemon.log");
7767
+ try {
7768
+ if (lstatSync(logPath).isSymbolicLink()) throw new ServeStoreError("invalid_state", "run log must not be a symbolic link");
7769
+ const resolvedLog = realpathSync(logPath);
7770
+ if (!isStrictDescendant(runDir, resolvedLog)) throw new ServeStoreError("invalid_state", "run log escapes its store");
7771
+ } catch (cause) {
7772
+ if (cause instanceof ServeStoreError) throw cause;
7773
+ if (cause.code !== "ENOENT") throw new ServeStoreError("io_error", "could not resolve run log", { cause });
7774
+ }
7775
+ return logPath;
7776
+ }
7777
+ createRunDir(id) {
7778
+ const dir = this.runDir(id);
7779
+ const piDir = join(dir, "pi");
7780
+ mkdirSync(piDir, {
7781
+ recursive: true,
7782
+ mode: 448
7783
+ });
7784
+ return {
7785
+ dir,
7786
+ piDir,
7787
+ logPath: join(dir, "daemon.log")
7788
+ };
7789
+ }
7790
+ readRun(id) {
7791
+ return readJson(join(this.runDir(id), "run.json"));
7792
+ }
7793
+ writeRun(record) {
7794
+ writeJsonAtomic(join(this.runDir(record.id), "run.json"), record);
7795
+ }
7796
+ listRuns(limit = Number.POSITIVE_INFINITY) {
7797
+ let ids;
7798
+ try {
7799
+ ids = readdirSync(this.runsDir);
7800
+ } catch {
7801
+ return [];
7802
+ }
7803
+ const sortedIds = ids.filter((id) => NAME_RE.test(id)).sort().reverse();
7804
+ const selectedIds = Number.isFinite(limit) ? sortedIds.slice(0, Math.max(0, limit)) : sortedIds;
7805
+ const records = [];
7806
+ for (const id of selectedIds) {
7807
+ const record = this.readRun(id);
7808
+ if (record) records.push(record);
7809
+ }
7810
+ return records.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
7811
+ }
7812
+ /** Remove only completed run directories outside the configured budget. */
7813
+ pruneCompletedRuns(options) {
7814
+ const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
7815
+ let retainedBytes = 0;
7816
+ let retainedCount = 0;
7817
+ const removed = [];
7818
+ for (const record of this.listRuns()) {
7819
+ if (record.status === "running") continue;
7820
+ const dir = this.runDir(record.id);
7821
+ const bytes = directoryBytes(dir);
7822
+ const endedAt = Date.parse(record.endedAt ?? record.startedAt);
7823
+ const expired = !Number.isFinite(endedAt) || now - endedAt > options.maxAgeMs;
7824
+ const overCount = retainedCount >= options.maxCount;
7825
+ const overBytes = retainedBytes + bytes > options.maxBytes;
7826
+ if (expired || overCount || overBytes) {
7827
+ rmSync(dir, {
7828
+ recursive: true,
7829
+ force: true
7830
+ });
7831
+ removed.push(record.id);
7832
+ continue;
7833
+ }
7834
+ retainedCount += 1;
7835
+ retainedBytes += bytes;
7836
+ }
7837
+ return removed;
7838
+ }
7839
+ };
7840
+ function directoryBytes(path) {
7841
+ let info;
7842
+ try {
7843
+ info = lstatSync(path);
7844
+ } catch {
7845
+ return 0;
7846
+ }
7847
+ if (info.isSymbolicLink()) return 0;
7848
+ if (!info.isDirectory()) return info.size;
7849
+ let total = 0;
7850
+ for (const entry of readdirSync(path, { withFileTypes: true })) total += directoryBytes(join(path, entry.name));
7851
+ return total;
7852
+ }
7853
+ function isRecord$1(value) {
7854
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7855
+ }
7856
+ function storeChildPath(root, kind, value, suffix = "") {
7857
+ const name = assertStoreName(kind, value);
7858
+ const normalizedRoot = resolve(root);
7859
+ const candidate = resolve(normalizedRoot, `${name}${suffix}`);
7860
+ if (!isStrictDescendant(normalizedRoot, candidate)) throw new ServeStoreError("invalid_name", `${kind} escapes its store`);
7861
+ return candidate;
7862
+ }
7863
+ function isStrictDescendant(root, candidate) {
7864
+ const rootPrefix = root.endsWith(sep) ? root : `${root}${sep}`;
7865
+ return candidate !== root && candidate.startsWith(rootPrefix);
7866
+ }
7867
+ function validateActivation(alias, value) {
7868
+ const invalid = () => {
7869
+ throw new ServeStoreError("invalid_state", `activation "${alias}" is not a valid version 1 activation`);
7870
+ };
7871
+ if (!isRecord$1(value)) invalid();
7872
+ const activation = value;
7873
+ if (activation.alias !== alias) invalid();
7874
+ if (![
7875
+ "identityId",
7876
+ "publicKey",
7877
+ "fingerprint",
7878
+ "createdAt"
7879
+ ].every((field) => typeof activation[field] === "string" && activation[field].length > 0)) invalid();
7880
+ if (activation.source === "managed") {
7881
+ if (typeof activation.apiUrl !== "string" || activation.apiUrl.length === 0 || activation.configPath !== void 0 || activation.configApiUrl !== void 0) invalid();
7882
+ return;
7883
+ }
7884
+ if (activation.source !== "external" || typeof activation.configPath !== "string" || activation.configPath.length === 0 || typeof activation.configApiUrl !== "string" || activation.configApiUrl.length === 0 || activation.apiUrl !== void 0 && typeof activation.apiUrl !== "string") invalid();
7885
+ }
7886
+ //#endregion
7887
+ //#region src/lib/serve/identity.ts
7888
+ /**
7889
+ * Serve identity activation (#2061/#1834 boundary).
7890
+ *
7891
+ * Managed agent files are canonical `MoltNetConfig` documents. Alias,
7892
+ * provenance, pinned identity material, and external config paths live only
7893
+ * in the versioned activation map in `serve.json`. Every attach and run loads
7894
+ * the current config and verifies it with authenticated `whoami` before use.
7895
+ */
7896
+ var MAX_CONFIG_BYTES = 64 * 1024;
7897
+ var IDENTITY_OPERATION_TIMEOUT_MS = 15e3;
7898
+ var pendingAliases = /* @__PURE__ */ new WeakMap();
7899
+ var ServeIdentityError = class extends Error {
7900
+ name = "ServeIdentityError";
7901
+ constructor(code, message, options) {
7902
+ super(message, options);
7903
+ this.code = code;
7904
+ }
7905
+ };
7906
+ function reserveAlias(store, alias) {
7907
+ let pending = pendingAliases.get(store);
7908
+ if (!pending) {
7909
+ pending = /* @__PURE__ */ new Set();
7910
+ pendingAliases.set(store, pending);
7911
+ }
7912
+ if (pending.has(alias) || store.hasPendingRegistration(alias) || store.readActivation(alias) || store.readAgentConfig(alias)) throw new ServeIdentityError("agent_exists", `agent "${alias}" already exists in the serve store`);
7913
+ pending.add(alias);
7914
+ return () => pending.delete(alias);
7915
+ }
7916
+ async function createManagedAgent(store, secrets, input, connectAgent = connect) {
7917
+ const alias = assertStoreName("agent name", input.name);
7918
+ if (!input.enrollmentToken.trim()) throw new ServeIdentityError("enrollment_required", "an enrollment token from the target team is required — a self-registered agent would be stranded in its own personal team");
7919
+ const releaseAlias = reserveAlias(store, alias);
7920
+ let registeredIdentityId;
7921
+ try {
7922
+ let apiUrl;
7923
+ try {
7924
+ apiUrl = requireSecureCredentialApiUrl(input.apiUrl);
7925
+ } catch (cause) {
7926
+ throw new ServeIdentityError("registration_failed", "registration API URL must use HTTPS or HTTP loopback", { cause });
7927
+ }
7928
+ store.reserveRegistration(alias, apiUrl);
7929
+ const result = await register({
7930
+ credentialType: "agent_key",
7931
+ apiUrl,
7932
+ enrollmentToken: input.enrollmentToken,
7933
+ signal: boundedIdentitySignal(input.signal)
7934
+ });
7935
+ if (result.credentials.type !== "agent_key") throw new ServeIdentityError("unsupported_credential", `registration returned credential type "${result.credentials.type}"; serve manages agent-key credentials only`);
7936
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7937
+ const { identityId, fingerprint, publicKey, privateKey } = result.identity;
7938
+ registeredIdentityId = identityId;
7939
+ const agentKeyReference = {
7940
+ provider: FILE_SECRET_PROVIDER,
7941
+ key: agentKeyKey(identityId)
7942
+ };
7943
+ const seedReference = {
7944
+ provider: FILE_SECRET_PROVIDER,
7945
+ key: identitySeedKey(fingerprint)
7946
+ };
7947
+ const config = {
7948
+ identity_id: identityId,
7949
+ registered_at: now,
7950
+ agent_key_ref: agentKeyReference,
7951
+ keys: {
7952
+ public_key: publicKey,
7953
+ fingerprint,
7954
+ private_key_ref: seedReference
7955
+ },
7956
+ endpoints: {
7957
+ api: result.apiUrl,
7958
+ mcp: deriveMcpUrl(result.apiUrl)
7959
+ }
7960
+ };
7961
+ store.writeAgentConfig(alias, config);
7962
+ await secrets.write(agentKeyReference.key, result.credentials.secret);
7963
+ await secrets.write(seedReference.key, privateKey);
7964
+ const whoami = await callWhoami(connectAgent, {
7965
+ agentKey: result.credentials.secret,
7966
+ apiUrl: result.apiUrl
7967
+ }, store.agentPath(alias), input.signal);
7968
+ assertIdentityMatches(whoami, {
7969
+ identityId,
7970
+ publicKey,
7971
+ fingerprint
7972
+ }, "authenticated whoami", `new managed agent "${alias}"`);
7973
+ const boundTeamId = boundTeamIdFromWhoami(whoami);
7974
+ const activation = {
7975
+ alias,
7976
+ source: "managed",
7977
+ identityId,
7978
+ publicKey,
7979
+ fingerprint,
7980
+ ...boundTeamId ? { boundTeamId } : {},
7981
+ createdAt: now,
7982
+ apiUrl: result.apiUrl
7983
+ };
7984
+ store.writeActivation(activation);
7985
+ return {
7986
+ activation,
7987
+ config,
7988
+ ...boundTeamId ? { boundTeamId } : {}
7989
+ };
7990
+ } catch (cause) {
7991
+ if (!registeredIdentityId && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
7992
+ store.clearPendingRegistration(alias);
7993
+ throw new ServeIdentityError("registration_failed", `registration for "${alias}" was rejected`, { cause });
7994
+ }
7995
+ if (store.hasPendingRegistration(alias)) throw new ServeIdentityError("registration_incomplete", registeredIdentityId ? `identity "${registeredIdentityId}" was registered but local activation is incomplete; reconcile or clear its pending serve record before retrying` : `registration for "${alias}" may be incomplete; inspect the remote API before changing its pending serve record`, { cause });
7996
+ throw cause;
7997
+ } finally {
7998
+ releaseAlias();
7999
+ }
8000
+ }
8001
+ /** Resume a fully persisted registration or explicitly abandon local recovery. */
8002
+ async function reconcileManagedRegistration(store, secrets, aliasInput, action, connectAgent = connect, signal) {
8003
+ const alias = assertStoreName("agent name", aliasInput);
8004
+ const activation = store.readActivation(alias);
8005
+ if (activation) {
8006
+ const config = store.readAgentConfig(alias);
8007
+ if (!config) throw new ServeIdentityError("registration_incomplete", `managed activation for "${alias}" is missing its canonical config`);
8008
+ return {
8009
+ activation,
8010
+ config
8011
+ };
8012
+ }
8013
+ if (!store.hasPendingRegistration(alias)) throw new ServeIdentityError("config_not_found", `agent "${alias}" has no pending registration to reconcile`);
8014
+ const config = store.readAgentConfig(alias);
8015
+ if (action === "abandon") {
8016
+ store.removeAgentConfig(alias);
8017
+ store.clearPendingRegistration(alias);
8018
+ return null;
8019
+ }
8020
+ if (!config?.agent_key_ref || config.agent_key_ref.provider !== FILE_SECRET_PROVIDER || config.keys.private_key_ref?.provider !== FILE_SECRET_PROVIDER) throw new ServeIdentityError("registration_incomplete", `pending registration for "${alias}" does not have complete managed references`);
8021
+ const [agentKey, privateKeyState] = await Promise.all([secrets.read(config.agent_key_ref.key), secrets.probe(config.keys.private_key_ref.key)]);
8022
+ if (!agentKey || privateKeyState !== "present") throw new ServeIdentityError("registration_incomplete", `pending registration for "${alias}" is missing persisted secret material`);
8023
+ const identity = identityFromConfig(config);
8024
+ const apiUrl = requireConfigApiUrl(config, store.agentPath(alias));
8025
+ const whoami = await callWhoami(connectAgent, {
8026
+ agentKey,
8027
+ apiUrl
8028
+ }, store.agentPath(alias), signal);
8029
+ assertIdentityMatches(whoami, identity, "authenticated whoami", `pending registration "${alias}" config`);
8030
+ const boundTeamId = boundTeamIdFromWhoami(whoami);
8031
+ const recovered = {
8032
+ alias,
8033
+ source: "managed",
8034
+ ...identity,
8035
+ ...boundTeamId ? { boundTeamId } : {},
8036
+ createdAt: config.registered_at,
8037
+ apiUrl
8038
+ };
8039
+ store.writeActivation(recovered);
8040
+ return {
8041
+ activation: recovered,
8042
+ config,
8043
+ ...recovered.boundTeamId ? { boundTeamId: recovered.boundTeamId } : {}
8044
+ };
8045
+ }
8046
+ async function attachExternalAgent(store, secretProviders, input, connectAgent = connect) {
8047
+ const alias = assertStoreName("agent name", input.name);
8048
+ const releaseAlias = reserveAlias(store, alias);
8049
+ try {
8050
+ if (!isAbsolute(input.configDir)) throw new ServeIdentityError("config_not_found", "external configDir must be an absolute path");
8051
+ const configPath = join(input.configDir, "moltnet.json");
8052
+ externalAgentLocation(configPath);
8053
+ const config = await readCurrentConfig(configPath);
8054
+ const configApiUrl = requireTrustedConfigApiUrl(config, configPath);
8055
+ const effectiveApiUrl = requireTrustedApiOverride(input.apiUrl, configApiUrl, configPath);
8056
+ const whoami = await authenticateConfig(input.configDir, effectiveApiUrl, secretProviders, connectAgent, input.signal);
8057
+ const identity = identityFromConfig(config);
8058
+ assertIdentityMatches(identity, whoami, `external config ${configPath}`, "authenticated whoami");
8059
+ const boundTeamId = boundTeamIdFromWhoami(whoami);
8060
+ const activation = {
8061
+ alias,
8062
+ source: "external",
8063
+ ...identity,
8064
+ ...boundTeamId ? { boundTeamId } : {},
8065
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8066
+ configPath,
8067
+ configApiUrl,
8068
+ ...input.apiUrl ? { apiUrl: input.apiUrl } : {}
8069
+ };
8070
+ store.writeActivation(activation);
8071
+ return {
8072
+ activation,
8073
+ config,
8074
+ ...activation.boundTeamId ? { boundTeamId: activation.boundTeamId } : {}
8075
+ };
8076
+ } finally {
8077
+ releaseAlias();
8078
+ }
8079
+ }
8080
+ /** Load and authenticate the current config, then compare all pinned fields. */
8081
+ async function verifyAgentActivation(store, alias, managedSecretProviders, externalSecretProviders, connectAgent = connect, signal) {
8082
+ const activation = requireActivation(store, alias);
8083
+ const verified = activation.source === "managed" ? await verifyManagedActivation(store, activation, managedSecretProviders, connectAgent, signal) : await verifyExternalActivation(activation, externalSecretProviders, connectAgent, signal);
8084
+ assertIdentityMatches(verified.whoami, activation, "authenticated whoami", `agent "${activation.alias}" pinned activation`);
8085
+ const boundTeamId = boundTeamIdFromWhoami(verified.whoami);
8086
+ if (activation.boundTeamId !== boundTeamId) throw new ServeIdentityError("verification_failed", `authenticated whoami team binding does not match agent "${activation.alias}" pinned activation`);
8087
+ return {
8088
+ activation,
8089
+ config: verified.config,
8090
+ ...boundTeamId ? { boundTeamId } : {}
8091
+ };
8092
+ }
8093
+ async function verifyManagedActivation(store, activation, secretProviders, connectAgent, signal) {
8094
+ const configPath = store.agentPath(activation.alias);
8095
+ const config = await readCurrentConfig(configPath);
8096
+ assertActivatedConfig(config, activation, configPath, requireConfigApiUrl(config, configPath), activation.apiUrl);
8097
+ let agentKey;
8098
+ try {
8099
+ agentKey = await resolveAgentKey(config, secretProviders);
8100
+ } catch (cause) {
8101
+ throw verificationError(`could not resolve the managed agent key for "${activation.alias}"`, cause);
8102
+ }
8103
+ if (!agentKey) throw new ServeIdentityError("verification_failed", `managed config for "${activation.alias}" has no agent_key_ref`);
8104
+ return {
8105
+ config,
8106
+ whoami: await callWhoami(connectAgent, {
8107
+ agentKey,
8108
+ apiUrl: activation.apiUrl
8109
+ }, configPath, signal)
8110
+ };
8111
+ }
8112
+ async function verifyExternalActivation(activation, secretProviders, connectAgent, signal) {
8113
+ externalAgentLocation(activation.configPath);
8114
+ assertTrustedConfigApiUrl(activation.configApiUrl);
8115
+ const config = await readCurrentConfig(activation.configPath);
8116
+ assertActivatedConfig(config, activation, activation.configPath, requireTrustedConfigApiUrl(config, activation.configPath), activation.configApiUrl);
8117
+ const effectiveApiUrl = requireTrustedApiOverride(activation.apiUrl, activation.configApiUrl, activation.configPath);
8118
+ return {
8119
+ config,
8120
+ whoami: await authenticateConfig(dirname(activation.configPath), effectiveApiUrl, secretProviders, connectAgent, signal)
8121
+ };
8122
+ }
8123
+ /** Never let request or activation metadata redirect persisted credentials. */
8124
+ function requireTrustedApiOverride(override, configApiUrl, configPath) {
8125
+ if (!override) return configApiUrl;
8126
+ if (override !== configApiUrl) throw new ServeIdentityError("verification_failed", `API override for ${configPath} does not match its configured endpoint`);
8127
+ return configApiUrl;
8128
+ }
8129
+ function assertActivatedConfig(config, activation, configPath, currentApiUrl, pinnedApiUrl) {
8130
+ if (currentApiUrl !== pinnedApiUrl) throw new ServeIdentityError("verification_failed", `agent config at ${configPath} API endpoint does not match its pinned activation`);
8131
+ assertIdentityMatches(identityFromConfig(config), activation, configPath, `agent "${activation.alias}" pinned activation`);
8132
+ }
8133
+ function requireConfigApiUrl(config, configPath) {
8134
+ const apiUrl = config?.endpoints?.api?.trim();
8135
+ if (!apiUrl) throw new ServeIdentityError("verification_failed", `agent config at ${configPath} is missing endpoints.api`);
8136
+ return apiUrl;
8137
+ }
8138
+ function requireTrustedConfigApiUrl(config, configPath) {
8139
+ const apiUrl = requireConfigApiUrl(config, configPath);
8140
+ try {
8141
+ assertTrustedConfigApiUrl(apiUrl);
8142
+ return apiUrl;
8143
+ } catch (cause) {
8144
+ throw verificationError(`agent config at ${configPath} has an untrusted endpoints.api`, cause);
8145
+ }
8146
+ }
8147
+ async function readCurrentConfig(configPath) {
8148
+ let file;
8149
+ try {
8150
+ file = await open(configPath, "r");
8151
+ } catch (cause) {
8152
+ throw new ServeIdentityError("config_not_found", `agent config is missing at ${configPath}`, { cause });
8153
+ }
8154
+ let raw;
8155
+ try {
8156
+ if (!(await file.stat()).isFile()) throw new ServeIdentityError("verification_failed", `agent config at ${configPath} must be a regular file no larger than ${MAX_CONFIG_BYTES} bytes`);
8157
+ const buffer = Buffer.alloc(MAX_CONFIG_BYTES + 1);
8158
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
8159
+ if (bytesRead > MAX_CONFIG_BYTES) throw new ServeIdentityError("verification_failed", `agent config at ${configPath} must be a regular file no larger than ${MAX_CONFIG_BYTES} bytes`);
8160
+ raw = buffer.toString("utf8", 0, bytesRead);
8161
+ } finally {
8162
+ await file.close();
8163
+ }
8164
+ try {
8165
+ return JSON.parse(raw);
8166
+ } catch (cause) {
8167
+ throw verificationError(`agent config is not valid JSON at ${configPath}`, cause);
8168
+ }
8169
+ }
8170
+ /** Resolve the unchanged daemon CLI's root/name pair for an external config. */
8171
+ function externalAgentLocation(configPath) {
8172
+ const configDir = dirname(configPath);
8173
+ const moltnetDir = dirname(configDir);
8174
+ if (!isAbsolute(configPath) || basename(configPath) !== "moltnet.json" || basename(moltnetDir) !== ".moltnet") throw new ServeIdentityError("config_not_found", `external config must be at an absolute <agent-root>/.moltnet/<agent>/moltnet.json path`);
8175
+ return {
8176
+ agentName: assertStoreName("external config agent name", basename(configDir)),
8177
+ agentRoot: dirname(moltnetDir)
8178
+ };
8179
+ }
8180
+ async function authenticateConfig(configDir, apiUrl, secretProviders, connectAgent, signal) {
8181
+ return callWhoami(connectAgent, {
8182
+ configDir,
8183
+ ...apiUrl ? { apiUrl } : {},
8184
+ secretProviders
8185
+ }, configDir, signal);
8186
+ }
8187
+ async function callWhoami(connectAgent, options, source, signal) {
8188
+ try {
8189
+ const boundedSignal = boundedIdentitySignal(signal);
8190
+ return await (await connectAgent({
8191
+ ...options,
8192
+ signal: boundedSignal
8193
+ })).agents.whoami({ signal: boundedSignal });
8194
+ } catch (cause) {
8195
+ throw verificationError(`could not authenticate ${source} against the API`, cause);
8196
+ }
8197
+ }
8198
+ function boundedIdentitySignal(signal) {
8199
+ const timeout = AbortSignal.timeout(IDENTITY_OPERATION_TIMEOUT_MS);
8200
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
8201
+ }
8202
+ function identityFromConfig(config) {
8203
+ const identityId = config?.identity_id?.trim();
8204
+ const publicKey = config?.keys?.public_key?.trim();
8205
+ const fingerprint = config?.keys?.fingerprint?.trim();
8206
+ if (!identityId || !publicKey || !fingerprint) throw new ServeIdentityError("verification_failed", "agent config is missing canonical identity_id, keys.public_key, or keys.fingerprint");
8207
+ return {
8208
+ identityId,
8209
+ publicKey,
8210
+ fingerprint
8211
+ };
8212
+ }
8213
+ function assertIdentityMatches(current, expected, currentLabel, expectedLabel) {
8214
+ const assessment = assessIdentityPin(current, expected);
8215
+ if (!assessment.ok) throw new ServeIdentityError("verification_failed", `${currentLabel} ${assessment.label} does not match ${expectedLabel}`);
8216
+ }
8217
+ function verificationError(message, cause) {
8218
+ return new ServeIdentityError("verification_failed", message, { cause });
8219
+ }
8220
+ /** Non-secret projection preserving the existing `/v1` response shape. */
8221
+ function publicAgentView(store, activation) {
8222
+ if (activation.source === "managed") {
8223
+ const config = store.readAgentConfig(activation.alias);
8224
+ return {
8225
+ kind: "managed",
8226
+ agentName: activation.alias,
8227
+ identityId: activation.identityId,
8228
+ fingerprint: activation.fingerprint,
8229
+ ...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
8230
+ apiUrl: activation.apiUrl,
8231
+ createdAt: activation.createdAt,
8232
+ hasAgentKey: Boolean(config?.agent_key_ref),
8233
+ hasPrivateKey: Boolean(config?.keys.private_key_ref)
8234
+ };
8235
+ }
8236
+ return {
8237
+ kind: "external",
8238
+ agentName: activation.alias,
8239
+ configDir: dirname(activation.configPath),
8240
+ ...activation.apiUrl ? { apiUrl: activation.apiUrl } : {},
8241
+ identityId: activation.identityId,
8242
+ fingerprint: activation.fingerprint,
8243
+ ...activation.boundTeamId ? { teamId: activation.boundTeamId } : {},
8244
+ createdAt: activation.createdAt
8245
+ };
8246
+ }
8247
+ function boundTeamIdFromWhoami(whoami) {
8248
+ return whoami.credentialBinding?.bindingScope === "team" ? whoami.credentialBinding.boundTeamId ?? void 0 : void 0;
8249
+ }
8250
+ function requireActivation(store, alias) {
8251
+ const activation = store.readActivation(alias);
8252
+ if (!activation) throw new ServeStoreError("not_found", `agent "${alias}" is not configured`);
8253
+ return activation;
8254
+ }
8255
+ //#endregion
8256
+ //#region src/lib/serve/runs.ts
8257
+ var STOP_GRACE_MS = 1e4;
8258
+ var STOP_FORCE_MS = 2e3;
8259
+ var DEFAULT_MAX_LOG_BYTES = 10 * 1024 * 1024;
8260
+ var DEFAULT_MAX_COMPLETED_RUNS = 100;
8261
+ var DEFAULT_MAX_RUN_AGE_MS = 720 * 60 * 60 * 1e3;
8262
+ var DEFAULT_MAX_RUN_STORAGE_BYTES = 512 * 1024 * 1024;
8263
+ var DEFAULT_MAX_ACTIVE_RUNS = 16;
8264
+ var DEFAULT_MAX_ACTIVE_RUNS_PER_AGENT = 4;
8265
+ var LOG_TRUNCATION_MARKER = Buffer.from("[truncated]\n");
8266
+ var INHERITED_ENV_NAMES = new Set([
8267
+ "COLORTERM",
8268
+ "FORCE_COLOR",
8269
+ "LANG",
8270
+ "LC_ALL",
8271
+ "LC_CTYPE",
8272
+ "LOG_LEVEL",
8273
+ "NO_COLOR",
8274
+ "PATH",
8275
+ "SSL_CERT_DIR",
8276
+ "SSL_CERT_FILE",
8277
+ "TEMP",
8278
+ "TERM",
8279
+ "TMP",
8280
+ "TMPDIR",
8281
+ "TZ"
8282
+ ]);
8283
+ var INHERITED_MOLTNET_ENV_NAMES = new Set([
8284
+ "MOLTNET_CLI_LINUX_BINARY",
8285
+ "MOLTNET_CREDENTIAL_BINDINGS",
8286
+ "MOLTNET_CREDENTIAL_ENFORCEMENT",
8287
+ "MOLTNET_DIARY_ID",
8288
+ "MOLTNET_GIT_AUTHOR",
8289
+ "MOLTNET_OTEL_ENDPOINT",
8290
+ "MOLTNET_PI_VM_INTEGRATION",
8291
+ "MOLTNET_PROFILE_CREDENTIAL_REQUIREMENTS",
8292
+ "MOLTNET_SIGNER_URL",
8293
+ "MOLTNET_TRACE_IDLE_POLLING"
8294
+ ]);
8295
+ var ServeRunError = class extends Error {
8296
+ name = "ServeRunError";
8297
+ constructor(code, message) {
8298
+ super(message);
8299
+ this.code = code;
8300
+ }
8301
+ };
8302
+ function validateRunSpec(spec) {
8303
+ if (spec.mode !== "poll" && spec.mode !== "drain") throw new ServeRunError("invalid_spec", "mode must be poll or drain");
8304
+ if (!spec.agent) throw new ServeRunError("invalid_spec", "agent is required");
8305
+ if (!spec.teamId) throw new ServeRunError("invalid_spec", "teamId is required");
8306
+ if (!Array.isArray(spec.profiles) || spec.profiles.length === 0) throw new ServeRunError("invalid_spec", "at least one profile is required");
8307
+ if (!Array.isArray(spec.taskTypes) || spec.taskTypes.length === 0) throw new ServeRunError("invalid_spec", "at least one task type is required");
8308
+ }
8309
+ var RunManager = class {
8310
+ active = /* @__PURE__ */ new Map();
8311
+ startingByAgent = /* @__PURE__ */ new Map();
8312
+ starting = 0;
8313
+ closing = false;
8314
+ constructor(options) {
8315
+ this.options = options;
8316
+ this.reconcileInterruptedRuns();
8317
+ this.pruneCompletedRuns();
8318
+ }
8319
+ get store() {
8320
+ return this.options.store;
8321
+ }
8322
+ entrypoint() {
8323
+ return this.options.entrypoint ?? {
8324
+ execPath: process.execPath,
8325
+ execArgv: process.execArgv,
8326
+ scriptPath: process.argv[1] ?? ""
8327
+ };
8328
+ }
8329
+ /** Assemble child env + args for a run. Exposed for tests. */
8330
+ async prepare(spec, agent, piDir, providers = this.store.readProviders()) {
8331
+ const { activation, config } = agent;
8332
+ const homeDir = join(dirname(piDir), "home");
8333
+ const env = {
8334
+ HOME: homeDir,
8335
+ PI_CODING_AGENT_DIR: piDir,
8336
+ XDG_CACHE_HOME: join(homeDir, ".cache"),
8337
+ XDG_CONFIG_HOME: join(homeDir, ".config"),
8338
+ XDG_DATA_HOME: join(homeDir, ".local", "share"),
8339
+ MOLTNET_TEAM_ID: spec.teamId
8340
+ };
8341
+ const target = activation.source === "managed" ? {
8342
+ agentName: activation.alias,
8343
+ cwd: dirname(piDir),
8344
+ extraArgs: []
8345
+ } : (() => {
8346
+ const { agentName, agentRoot } = externalAgentLocation(activation.configPath);
8347
+ return {
8348
+ agentName,
8349
+ cwd: agentRoot,
8350
+ extraArgs: ["--agent-root", agentRoot]
8351
+ };
8352
+ })();
8353
+ const args = [
8354
+ spec.mode,
8355
+ "--agent",
8356
+ target.agentName,
8357
+ "--team",
8358
+ spec.teamId,
8359
+ ...spec.profiles.flatMap((profile) => ["--profile", profile]),
8360
+ "--task-types",
8361
+ spec.taskTypes.join(","),
8362
+ ...target.extraArgs
8363
+ ];
8364
+ if (activation.source === "managed") {
8365
+ if (!config.agent_key_ref || !config.keys.private_key_ref) throw new ServeRunError("invalid_spec", `managed config for "${activation.alias}" is missing canonical secret references`);
8366
+ env["MOLTNET_API_URL"] = activation.apiUrl;
8367
+ env["MOLTNET_AGENT_KEY_REF"] = formatSecretReferenceString(config.agent_key_ref);
8368
+ env["MOLTNET_PRIVATE_KEY_REF"] = formatSecretReferenceString(config.keys.private_key_ref);
8369
+ env["MOLTNET_SECRET_ROOT"] = this.store.secretsDir;
8370
+ } else {
8371
+ env["MOLTNET_API_URL"] = activation.apiUrl ?? activation.configApiUrl;
8372
+ try {
8373
+ const agentKey = await resolveAgentKey(config, this.options.externalSecretProviders);
8374
+ if (agentKey) {
8375
+ env["MOLTNET_AGENT_KEY"] = agentKey;
8376
+ env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
8377
+ } else if (config.oauth2?.client_secret_ref) {
8378
+ env["MOLTNET_CLIENT_ID"] = config.oauth2.client_id;
8379
+ env["MOLTNET_CLIENT_SECRET"] = await resolveOAuth2ClientSecret(config, this.options.externalSecretProviders);
8380
+ }
8381
+ if (!agentKey && config.keys.private_key_ref) env["MOLTNET_PRIVATE_KEY"] = await resolveIdentitySeed(config, this.options.externalSecretProviders);
8382
+ } catch {
8383
+ throw new ServeRunError("invalid_spec", `external credentials for "${activation.alias}" could not be projected`);
8384
+ }
8385
+ }
8386
+ env["MOLTNET_EXPECTED_IDENTITY_ID"] = activation.identityId;
8387
+ env["MOLTNET_EXPECTED_PUBLIC_KEY"] = activation.publicKey;
8388
+ env["MOLTNET_EXPECTED_FINGERPRINT"] = activation.fingerprint;
8389
+ env["MOLTNET_SUPERVISED_RUN"] = "1";
8390
+ for (const [providerId, provider] of Object.entries(providers)) {
8391
+ if (!provider.apiKeyRef) continue;
8392
+ let value;
8393
+ try {
8394
+ value = await this.options.secretProviders.resolve(parseSecretReferenceString(provider.apiKeyRef));
8395
+ } catch {
8396
+ throw new ServeRunError("invalid_spec", `provider "${providerId}" API key could not be resolved`);
8397
+ }
8398
+ env[provider.envName] = value;
8399
+ }
8400
+ return {
8401
+ args,
8402
+ env,
8403
+ cwd: target.cwd
8404
+ };
8405
+ }
8406
+ async start(spec, signal) {
8407
+ validateRunSpec(spec);
8408
+ const releaseStart = this.reserveStart(spec.agent);
8409
+ try {
8410
+ return await this.startReserved(spec, signal);
8411
+ } finally {
8412
+ releaseStart();
8413
+ }
8414
+ }
8415
+ async startReserved(spec, signal) {
8416
+ this.assertStartOpen(signal);
8417
+ const agent = await (this.options.verifyActivationImpl ?? verifyAgentActivation)(this.store, spec.agent, this.options.secretProviders, this.options.externalSecretProviders, void 0, signal);
8418
+ this.assertStartOpen(signal);
8419
+ if (agent.boundTeamId && agent.boundTeamId !== spec.teamId) throw new ServeRunError("invalid_spec", `agent "${spec.agent}" has a key bound to team ${agent.boundTeamId}; start the run with that team, or create a new agent with an enrollment token from team ${spec.teamId}`);
8420
+ const id = `${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
8421
+ const runDir = this.store.runDir(id);
8422
+ const piDir = join(runDir, "pi");
8423
+ const providers = this.store.readProviders();
8424
+ const { args, env, cwd } = await this.prepare(spec, agent, piDir, providers);
8425
+ this.assertStartOpen(signal);
8426
+ let child;
8427
+ let logStream;
8428
+ let logLimiter;
8429
+ try {
8430
+ const { logPath } = this.store.createRunDir(id);
8431
+ for (const dir of [
8432
+ env.HOME,
8433
+ env.XDG_CACHE_HOME,
8434
+ env.XDG_CONFIG_HOME,
8435
+ env.XDG_DATA_HOME
8436
+ ]) mkdirSync(dir, {
8437
+ recursive: true,
8438
+ mode: 448
8439
+ });
8440
+ writePiConfig({
8441
+ piDir,
8442
+ providers: Object.fromEntries(Object.entries(providers).map(([providerId, provider]) => [providerId, {
8443
+ api: provider.api,
8444
+ ...provider.apiKeyRef ? { apiKeyEnvRef: `$${provider.envName}` } : {},
8445
+ baseUrl: provider.baseUrl,
8446
+ models: provider.models
8447
+ }]))
8448
+ });
8449
+ try {
8450
+ (this.options.symlinkImpl ?? symlinkSync)(this.store.piAuthJsonPath, join(piDir, "auth.json"));
8451
+ } catch (cause) {
8452
+ throw new ServeStoreError("io_error", "could not link subscription credentials into the run", { cause });
8453
+ }
8454
+ const entry = this.entrypoint();
8455
+ logStream = createWriteStream(logPath, {
8456
+ fd: openSync(logPath, "a", 384),
8457
+ autoClose: true
8458
+ });
8459
+ logLimiter = createByteLimitTransform(this.options.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES, () => this.log("warn", "serve run log output truncated", {
8460
+ ...runContext(id, spec.agent, child),
8461
+ transition: "log_truncated"
8462
+ }));
8463
+ logLimiter.pipe(logStream);
8464
+ logStream.on("error", (error) => {
8465
+ this.log("error", "serve run log stream failed", {
8466
+ ...runContext(id, spec.agent, child),
8467
+ transition: "log_failed",
8468
+ ...safeRunError(error)
8469
+ });
8470
+ child?.kill("SIGKILL");
8471
+ });
8472
+ const spawnImpl = this.options.spawnImpl ?? spawn;
8473
+ this.assertStartOpen(signal);
8474
+ child = spawnImpl(entry.execPath, [
8475
+ ...entry.execArgv,
8476
+ entry.scriptPath,
8477
+ ...args
8478
+ ], {
8479
+ cwd,
8480
+ env: {
8481
+ ...sanitizeChildBaseEnv(this.options.baseEnv),
8482
+ ...env
8483
+ },
8484
+ stdio: [
8485
+ "ignore",
8486
+ "pipe",
8487
+ "pipe",
8488
+ "ipc"
8489
+ ],
8490
+ detached: false
8491
+ });
8492
+ this.active.set(id, {
8493
+ agent: spec.agent,
8494
+ child,
8495
+ stopRequested: false
8496
+ });
8497
+ child.stdout?.pipe(logLimiter, { end: false });
8498
+ child.stderr?.pipe(logLimiter, { end: false });
8499
+ const record = {
8500
+ ...spec,
8501
+ id,
8502
+ status: "running",
8503
+ pid: child.pid,
8504
+ startedAt: (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
8505
+ };
8506
+ child.once("exit", (code, signal) => {
8507
+ const activeRun = this.active.get(id);
8508
+ this.active.delete(id);
8509
+ logLimiter?.end();
8510
+ const status = activeRun?.stopRequested ? "stopped" : code === 0 ? "exited" : "failed";
8511
+ this.log(status === "failed" ? "error" : "info", "serve run exited", {
8512
+ ...runContext(id, spec.agent, child),
8513
+ transition: status,
8514
+ exitCode: code,
8515
+ signal
8516
+ });
8517
+ this.persistRunCompletion(id, spec.agent, {
8518
+ status,
8519
+ exitCode: code
8520
+ });
8521
+ });
8522
+ child.once("error", (error) => {
8523
+ this.active.delete(id);
8524
+ logLimiter?.end();
8525
+ this.log("error", "serve run child process failed", {
8526
+ ...runContext(id, spec.agent, child),
8527
+ transition: "spawn_failed",
8528
+ ...safeRunError(error)
8529
+ });
8530
+ this.persistRunCompletion(id, spec.agent, { status: "failed" });
8531
+ });
8532
+ this.store.writeRun(record);
8533
+ this.log("info", "serve run started", {
8534
+ ...runContext(id, spec.agent, child),
8535
+ transition: "running"
8536
+ });
8537
+ return record;
8538
+ } catch (cause) {
8539
+ logLimiter?.destroy();
8540
+ logStream?.destroy();
8541
+ if (child && !await terminateChild(child)) throw new AggregateError([cause], `run "${id}" failed to start and its child did not exit after SIGKILL`);
8542
+ this.active.delete(id);
8543
+ rmSync(runDir, {
8544
+ recursive: true,
8545
+ force: true
8546
+ });
8547
+ this.log("error", "serve run failed to start", {
8548
+ ...runContext(id, spec.agent, child),
8549
+ transition: "start_failed",
8550
+ ...safeRunError(cause)
8551
+ });
8552
+ throw cause;
8553
+ }
8554
+ }
8555
+ stop(id) {
8556
+ const record = this.store.readRun(id);
8557
+ if (!record) throw new ServeRunError("run_not_found", `run "${id}" was not found`);
8558
+ const activeRun = this.active.get(id);
8559
+ if (!activeRun) throw new ServeRunError("run_not_active", `run "${id}" is not running`);
8560
+ activeRun.stopRequested = true;
8561
+ this.log("info", "serve run stop requested", {
8562
+ ...runContext(id, record.agent, activeRun.child),
8563
+ transition: "stopping",
8564
+ signal: "SIGTERM"
8565
+ });
8566
+ activeRun.child.kill("SIGTERM");
8567
+ setTimeout(() => {
8568
+ if (this.active.has(id)) activeRun.child.kill("SIGKILL");
8569
+ }, STOP_GRACE_MS).unref();
8570
+ return record;
8571
+ }
8572
+ /** Live status merged over the persisted record. */
8573
+ status(id) {
8574
+ const record = this.store.readRun(id);
8575
+ if (!record) throw new ServeStoreError("not_found", `run "${id}" was not found`);
8576
+ return record;
8577
+ }
8578
+ list(limit = Number.POSITIVE_INFINITY) {
8579
+ if (!Number.isFinite(limit)) return this.store.listRuns();
8580
+ const active = [...this.active.keys()].map((id) => this.store.readRun(id)).filter((record) => record !== null).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
8581
+ const activeIds = new Set(active.map((record) => record.id));
8582
+ const history = this.store.listRuns(limit + active.length).filter((record) => !activeIds.has(record.id)).slice(0, limit);
8583
+ return [...active, ...history];
8584
+ }
8585
+ isActive(id) {
8586
+ return this.active.has(id);
8587
+ }
8588
+ async stopAll() {
8589
+ this.closing = true;
8590
+ const waits = [];
8591
+ for (const [id, activeRun] of this.active) {
8592
+ activeRun.stopRequested = true;
8593
+ waits.push(new Promise((resolvePromise, rejectPromise) => {
8594
+ let settled = false;
8595
+ const finish = () => {
8596
+ if (settled) return;
8597
+ settled = true;
8598
+ clearTimeout(killTimer);
8599
+ clearTimeout(forceTimer);
8600
+ resolvePromise();
8601
+ };
8602
+ activeRun.child.once("exit", finish);
8603
+ activeRun.child.kill("SIGTERM");
8604
+ const killTimer = setTimeout(() => {
8605
+ activeRun.child.kill("SIGKILL");
8606
+ }, STOP_GRACE_MS);
8607
+ killTimer.unref();
8608
+ const forceTimer = setTimeout(() => {
8609
+ if (settled) return;
8610
+ settled = true;
8611
+ clearTimeout(killTimer);
8612
+ rejectPromise(/* @__PURE__ */ new Error(`run "${id}" did not exit after SIGKILL`));
8613
+ }, STOP_GRACE_MS + STOP_FORCE_MS);
8614
+ forceTimer.unref();
8615
+ }));
8616
+ }
8617
+ await Promise.all(waits);
8618
+ }
8619
+ forceStopAll() {
8620
+ this.closing = true;
8621
+ for (const [id, activeRun] of this.active) {
8622
+ activeRun.stopRequested = true;
8623
+ this.log("warn", "serve run force stop requested", {
8624
+ ...runContext(id, this.store.readRun(id)?.agent ?? "unknown", activeRun.child),
8625
+ transition: "force_stopping",
8626
+ signal: "SIGKILL"
8627
+ });
8628
+ activeRun.child.kill("SIGKILL");
8629
+ }
8630
+ }
8631
+ reconcileInterruptedRuns() {
8632
+ const endedAt = (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
8633
+ for (const record of this.store.listRuns()) {
8634
+ if (record.status !== "running") continue;
8635
+ this.store.writeRun({
8636
+ ...record,
8637
+ status: "failed",
8638
+ exitCode: null,
8639
+ endedAt
8640
+ });
8641
+ this.log("warn", "serve run interrupted by supervisor replacement", {
8642
+ runId: record.id,
8643
+ agent: record.agent,
8644
+ pid: record.pid,
8645
+ transition: "interrupted"
8646
+ });
8647
+ }
8648
+ }
8649
+ reserveStart(agent) {
8650
+ if (this.closing) throw new ServeRunError("invalid_spec", "serve is shutting down");
8651
+ const agentStarting = this.startingByAgent.get(agent) ?? 0;
8652
+ const agentActive = [...this.active.values()].filter((run) => run.agent === agent).length;
8653
+ if (this.active.size + this.starting >= (this.options.maxActiveRuns ?? DEFAULT_MAX_ACTIVE_RUNS) || agentActive + agentStarting >= (this.options.maxActiveRunsPerAgent ?? DEFAULT_MAX_ACTIVE_RUNS_PER_AGENT)) throw new ServeRunError("invalid_spec", `active run limit reached for agent "${agent}"`);
8654
+ this.starting += 1;
8655
+ this.startingByAgent.set(agent, agentStarting + 1);
8656
+ return () => {
8657
+ this.starting -= 1;
8658
+ const remaining = (this.startingByAgent.get(agent) ?? 1) - 1;
8659
+ if (remaining > 0) this.startingByAgent.set(agent, remaining);
8660
+ else this.startingByAgent.delete(agent);
8661
+ };
8662
+ }
8663
+ assertStartOpen(signal) {
8664
+ if (this.closing || signal?.aborted) throw new ServeRunError("invalid_spec", "serve is shutting down");
8665
+ }
8666
+ pruneCompletedRuns() {
8667
+ const removed = this.store.pruneCompletedRuns({
8668
+ maxCount: this.options.maxCompletedRuns ?? DEFAULT_MAX_COMPLETED_RUNS,
8669
+ maxAgeMs: this.options.maxRunAgeMs ?? DEFAULT_MAX_RUN_AGE_MS,
8670
+ maxBytes: this.options.maxRunStorageBytes ?? DEFAULT_MAX_RUN_STORAGE_BYTES,
8671
+ now: this.options.now?.() ?? /* @__PURE__ */ new Date()
8672
+ });
8673
+ if (removed.length > 0) this.log("info", "serve run retention removed completed artifacts", {
8674
+ transition: "pruned",
8675
+ removedCount: removed.length
8676
+ });
8677
+ }
8678
+ persistRunCompletion(id, agent, update) {
8679
+ try {
8680
+ const current = this.store.readRun(id);
8681
+ if (current) this.store.writeRun({
8682
+ ...current,
8683
+ ...update,
8684
+ endedAt: (this.options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
8685
+ });
8686
+ this.pruneCompletedRuns();
8687
+ } catch (error) {
8688
+ this.log("error", "serve run completion persistence failed", {
8689
+ runId: id,
8690
+ agent,
8691
+ transition: "persistence_failed",
8692
+ ...safeRunError(error)
8693
+ });
8694
+ }
8695
+ }
8696
+ log(level, message, context) {
8697
+ this.options.logger?.[level](context, message);
8698
+ }
8699
+ };
8700
+ function createByteLimitTransform(limit, onTruncated) {
8701
+ const byteLimit = Math.max(0, Math.floor(limit));
8702
+ const contentLimit = Math.max(0, byteLimit - LOG_TRUNCATION_MARKER.length);
8703
+ let emitted = 0;
8704
+ let truncated = false;
8705
+ return new Transform({ transform(chunk, _encoding, callback) {
8706
+ const remaining = Math.max(0, contentLimit - emitted);
8707
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8708
+ if (remaining > 0) {
8709
+ const selected = bytes.subarray(0, remaining);
8710
+ emitted += selected.length;
8711
+ this.push(selected);
8712
+ }
8713
+ if (!truncated && bytes.length > remaining) {
8714
+ truncated = true;
8715
+ const marker = LOG_TRUNCATION_MARKER.subarray(0, byteLimit - emitted);
8716
+ emitted += marker.length;
8717
+ this.push(marker);
8718
+ onTruncated();
8719
+ }
8720
+ callback();
8721
+ } });
8722
+ }
8723
+ function runContext(runId, agent, child) {
8724
+ return {
8725
+ runId,
8726
+ agent,
8727
+ pid: child?.pid
8728
+ };
8729
+ }
8730
+ function safeRunError(error) {
8731
+ const code = error?.code;
8732
+ return {
8733
+ errorType: error instanceof Error ? error.name : typeof error,
8734
+ ...typeof code === "string" ? { errorCode: code } : {}
8735
+ };
8736
+ }
8737
+ function sanitizeChildBaseEnv(baseEnv) {
8738
+ return Object.fromEntries(Object.entries(baseEnv).filter(([name]) => INHERITED_ENV_NAMES.has(name) || INHERITED_MOLTNET_ENV_NAMES.has(name)));
8739
+ }
8740
+ async function terminateChild(child) {
8741
+ if (child.exitCode !== null && child.exitCode !== void 0) return true;
8742
+ return new Promise((resolvePromise) => {
8743
+ const timer = setTimeout(() => resolvePromise(false), STOP_FORCE_MS);
8744
+ timer.unref();
8745
+ child.once("exit", () => {
8746
+ clearTimeout(timer);
8747
+ resolvePromise(true);
8748
+ });
8749
+ child.kill("SIGKILL");
8750
+ });
8751
+ }
8752
+ //#endregion
8753
+ //#region src/lib/serve/serve-lock.ts
8754
+ var ServeLockError = class extends Error {
8755
+ name = "ServeLockError";
8756
+ constructor(code, message, options) {
8757
+ super(message, options);
8758
+ this.code = code;
8759
+ }
8760
+ };
8761
+ /**
8762
+ * Acquire the per-root singleton lock. `proper-lockfile` uses an atomic lock
8763
+ * directory at exactly `<root>/serve.lock`, recovers stale owners, and keeps
8764
+ * the mtime fresh while the supervisor is alive.
8765
+ */
8766
+ async function acquireServeLock(root, options = {}) {
8767
+ const path = join(root, "serve.lock");
8768
+ let releaseLock;
8769
+ try {
8770
+ releaseLock = await lock(root, {
8771
+ lockfilePath: path,
8772
+ realpath: false,
8773
+ retries: 0,
8774
+ ...options.staleMs === void 0 ? {} : { stale: options.staleMs },
8775
+ ...options.updateMs === void 0 ? {} : { update: options.updateMs },
8776
+ onCompromised: (cause) => {
8777
+ const error = new ServeLockError("compromised", `serve lock ${path} was compromised: ${cause.message}`, { cause });
8778
+ if (options.onCompromised) {
8779
+ options.onCompromised(error);
8780
+ return;
8781
+ }
8782
+ throw error;
8783
+ }
8784
+ });
8785
+ } catch (cause) {
8786
+ if (cause.code === "ELOCKED") throw new ServeLockError("held", `another moltnet-agent serve process already owns ${path}`, { cause });
8787
+ throw new ServeLockError("failed", `could not acquire serve lock ${path}: ${cause.message}`, { cause });
8788
+ }
8789
+ let released = false;
8790
+ return {
8791
+ path,
8792
+ async release() {
8793
+ if (released) return;
8794
+ released = true;
8795
+ await releaseLock();
8796
+ }
8797
+ };
8798
+ }
8799
+ /** Always release after normal completion or startup/runtime failure. */
8800
+ async function withServeLock(root, work, options) {
8801
+ const held = await acquireServeLock(root, options);
8802
+ try {
8803
+ return await work();
8804
+ } finally {
8805
+ await held.release();
8806
+ }
8807
+ }
8808
+ var ServeModelDiscoveryError = class extends Error {
8809
+ name = "ServeModelDiscoveryError";
8810
+ constructor(code, message, statusCode, options) {
8811
+ super(message, options);
8812
+ this.code = code;
8813
+ this.statusCode = statusCode;
8814
+ }
8815
+ };
8816
+ var ModelDiscoveryCollector = class {
8817
+ models = /* @__PURE__ */ new Set();
8818
+ addOpenAiResponse(value) {
8819
+ if (!isRecord(value) || !Array.isArray(value["data"])) return;
8820
+ for (const candidate of value["data"]) {
8821
+ if (!isRecord(candidate)) continue;
8822
+ const id = candidate["id"];
8823
+ if (typeof id === "string" && id.length > 0) this.models.add(id);
8824
+ }
8825
+ }
8826
+ addOllamaResponse(value) {
8827
+ if (!isRecord(value) || !Array.isArray(value["models"])) return;
8828
+ for (const candidate of value["models"]) {
8829
+ if (!isRecord(candidate)) continue;
8830
+ const name = candidate["name"];
8831
+ if (typeof name === "string" && name.length > 0) this.models.add(name);
8832
+ }
8833
+ }
8834
+ get size() {
8835
+ return this.models.size;
8836
+ }
8837
+ result(providerId, failures) {
8838
+ if (this.models.size === 0) throw discoveryFailure(providerId, failures);
8839
+ return {
8840
+ models: [...this.models].sort().slice(0, 500),
8841
+ discoveredCount: this.models.size
8842
+ };
8843
+ }
8844
+ };
8845
+ function parseProviderBaseUrl(value, providerId) {
8846
+ let parsed;
8847
+ try {
8848
+ parsed = new URL(value);
8849
+ } catch (cause) {
8850
+ throw new ServeModelDiscoveryError("invalid_provider", `provider "${providerId}" has an invalid base URL`, 400, { cause });
8851
+ }
8852
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash) throw new ServeModelDiscoveryError("invalid_provider", `provider "${providerId}" base URL must be HTTP(S) without credentials, query, or fragment`, 400);
8853
+ return parsed;
8854
+ }
8855
+ function discoveryFailure(providerId, failures) {
8856
+ if (failures.some((failure) => failure.kind === "http" && (failure.status === 401 || failure.status === 403))) return new ServeModelDiscoveryError("discovery_unauthorized", `provider "${providerId}" rejected model discovery; check its API key`, 502);
8857
+ if (failures.some((failure) => failure.kind === "network")) return new ServeModelDiscoveryError("discovery_unavailable", `provider "${providerId}" could not be reached for model discovery`, 502);
8858
+ if (failures.some((failure) => failure.kind === "invalid_response")) return new ServeModelDiscoveryError("discovery_invalid_response", `provider "${providerId}" returned an invalid model response`, 502);
8859
+ return new ServeModelDiscoveryError("discovery_failed", `no models discovered for provider "${providerId}"`, 502);
8860
+ }
8861
+ function isRecord(value) {
8862
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8863
+ }
8864
+ //#endregion
8865
+ //#region src/lib/serve/server.ts
8866
+ /**
8867
+ * HTTP surface of `moltnet-agent serve` (#2061), built on the shared
8868
+ * loopback-companion security profile (#2066): loopback Host enforcement,
8869
+ * exact-origin CORS, Fetch-Metadata guards, strict JSON parsing.
8870
+ *
8871
+ * Everything under `/v1` except the pairing bootstrap requires a paired
8872
+ * origin: the `x-moltnet-serve-token` header must verify against the
8873
+ * origin-bound token issued by the one-click pairing ceremony.
8874
+ */
8875
+ var SERVE_TOKEN_HEADER = "x-moltnet-serve-token";
8876
+ var BODY_LIMIT = 64 * 1024;
8877
+ var LOG_POLL_INTERVAL_MS = 500;
8878
+ var LOG_STREAM_MAX_DURATION_MS = 3600 * 1e3;
8879
+ var MAX_LOG_STREAMS = 32;
8880
+ var LOG_READ_LIMIT_BYTES = 256 * 1024;
8881
+ var RUN_HISTORY_LIMIT = 100;
8882
+ var RATE_LIMIT_MAX = 120;
8883
+ var RATE_LIMIT_WINDOW_MS = 6e4;
8884
+ async function readServeLogDelta(handle, state, limit = LOG_READ_LIMIT_BYTES) {
8885
+ const info = await handle.stat();
8886
+ if (!info.isFile() || info.size <= state.offset) return {
8887
+ lines: [],
8888
+ omitted: false
8889
+ };
8890
+ const size = info.size;
8891
+ const start = Math.max(state.offset, size - limit);
8892
+ let omitted = start > state.offset;
8893
+ const buffer = Buffer.alloc(size - start);
8894
+ let totalRead = 0;
8895
+ while (totalRead < buffer.length) {
8896
+ const { bytesRead } = await handle.read(buffer, totalRead, buffer.length - totalRead, start + totalRead);
8897
+ if (bytesRead === 0) break;
8898
+ totalRead += bytesRead;
8899
+ }
8900
+ state.offset = start + totalRead;
8901
+ if (omitted) {
8902
+ state.fragment = "";
8903
+ state.decoder = new StringDecoder("utf8");
8904
+ state.discardingLine = true;
8905
+ }
8906
+ state.decoder ??= new StringDecoder("utf8");
8907
+ let text = state.fragment + state.decoder.write(buffer.subarray(0, totalRead));
8908
+ state.fragment = "";
8909
+ if (state.discardingLine) {
8910
+ const boundary = text.indexOf("\n");
8911
+ if (boundary === -1) return {
8912
+ lines: [],
8913
+ omitted
8914
+ };
8915
+ text = text.slice(boundary + 1);
8916
+ state.discardingLine = false;
8917
+ }
8918
+ const parts = text.split("\n");
8919
+ const fragment = parts.pop() ?? "";
8920
+ const lines = [];
8921
+ for (const line of parts) if (line.length > limit) omitted = true;
8922
+ else if (line.length > 0) lines.push(line);
8923
+ if (fragment.length > limit) {
8924
+ state.discardingLine = true;
8925
+ omitted = true;
8926
+ } else state.fragment = fragment;
8927
+ return {
8928
+ lines,
8929
+ omitted
8930
+ };
8931
+ }
8932
+ var ServeHttpError = class extends Error {
8933
+ name = "ServeHttpError";
8934
+ constructor(statusCode, code, message) {
8935
+ super(message);
8936
+ this.statusCode = statusCode;
8937
+ this.code = code;
8938
+ }
8939
+ };
8940
+ function requireBody(request) {
8941
+ const body = request.body;
8942
+ if (typeof body !== "object" || body === null || Array.isArray(body)) throw new ServeHttpError(400, "invalid_body", "JSON object body required");
8943
+ return body;
8944
+ }
8945
+ function requireString(body, field) {
8946
+ const value = body[field];
8947
+ if (typeof value !== "string" || value.trim().length === 0) throw new ServeHttpError(400, "invalid_body", `"${field}" must be a non-empty string`);
8948
+ return value.trim();
8949
+ }
8950
+ function optionalString(body, field) {
8951
+ const value = body[field];
8952
+ if (value === void 0 || value === null) return void 0;
8953
+ if (typeof value !== "string" || value.trim().length === 0) throw new ServeHttpError(400, "invalid_body", `"${field}" must be a non-empty string when present`);
8954
+ return value.trim();
8955
+ }
8956
+ function stringArray(body, field, options = {}) {
8957
+ const value = body[field];
8958
+ if (!Array.isArray(value) || !options.allowEmpty && value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new ServeHttpError(400, "invalid_body", `"${field}" must be ${options.allowEmpty ? "a" : "a non-empty"} string array`);
8959
+ return value;
8960
+ }
8961
+ function requestOperationSignal(request, shutdownSignal) {
8962
+ const disconnected = new AbortController();
8963
+ if (request.raw.aborted) disconnected.abort();
8964
+ else request.raw.once("aborted", () => disconnected.abort());
8965
+ return shutdownSignal ? AbortSignal.any([disconnected.signal, shutdownSignal]) : disconnected.signal;
8966
+ }
8967
+ function buildServeServer(options) {
8968
+ const { pairing } = options;
8969
+ const app = options.logger ? Fastify({
8970
+ bodyLimit: BODY_LIMIT,
8971
+ loggerInstance: options.logger
8972
+ }) : Fastify({ bodyLimit: BODY_LIMIT });
8973
+ registerLoopbackSecurity(app, {
8974
+ allowedOrigins: options.allowedOrigins,
8975
+ ...options.selfOrigin ? { selfOrigins: [options.selfOrigin] } : {},
8976
+ allowedHeaders: [SERVE_TOKEN_HEADER],
8977
+ methods: [
8978
+ "GET",
8979
+ "POST",
8980
+ "PUT",
8981
+ "DELETE",
8982
+ "OPTIONS"
8983
+ ]
8984
+ });
8985
+ app.register(rateLimit, {
8986
+ global: false,
8987
+ max: options.rateLimitMax ?? RATE_LIMIT_MAX,
8988
+ timeWindow: RATE_LIMIT_WINDOW_MS,
8989
+ errorResponseBuilder: () => new ServeHttpError(429, "rate_limited", "Too many requests"),
8990
+ keyGenerator: (request) => {
8991
+ const origin = request.headers.origin;
8992
+ return isConfiguredOrigin(origin, options) ? `origin:${origin}` : `ip:${request.ip}`;
8993
+ }
8994
+ });
8995
+ app.after(() => app.addHook("onRequest", app.rateLimit()));
8996
+ app.addContentTypeParser("application/x-www-form-urlencoded", { parseAs: "buffer" }, (_request, body, done) => {
8997
+ try {
8998
+ const form = new TextDecoder("utf-8", { fatal: true }).decode(typeof body === "string" ? Buffer.from(body) : body);
8999
+ done(null, new URLSearchParams(form));
9000
+ } catch {
9001
+ done(new ServeHttpError(400, "invalid_body", "Form body is invalid"), void 0);
9002
+ }
9003
+ });
9004
+ const requirePairedOrigin = (request) => {
9005
+ const origin = requireOriginHeader(request.headers);
9006
+ const token = request.headers[SERVE_TOKEN_HEADER];
9007
+ if (typeof token !== "string" || token.length === 0) throw new ServeHttpError(401, "pairing_required", "Pairing token is required");
9008
+ pairing.verify(origin, token);
9009
+ return origin;
9010
+ };
9011
+ app.get("/health", async () => ({ status: "ok" }));
9012
+ registerPairingRoutes(app, pairing);
9013
+ registerStatusRoute(app, options, requirePairedOrigin);
9014
+ registerAgentRoutes(app, options, requirePairedOrigin);
9015
+ registerProviderRoutes(app, options, requirePairedOrigin);
9016
+ registerSubscriptionRoutes(app, options, requirePairedOrigin);
9017
+ registerRunRoutes(app, options, requirePairedOrigin);
9018
+ app.addHook("onClose", () => {
9019
+ options.subscriptions.close();
9020
+ });
9021
+ app.setNotFoundHandler(async (_request, reply) => reply.code(404).send({
9022
+ code: "not_found",
9023
+ message: "Route is not available"
9024
+ }));
9025
+ app.setErrorHandler(async (error, request, reply) => {
9026
+ const { statusCode, code, message } = normalizeServeError(error);
9027
+ if (statusCode === 500) request.log.error({
9028
+ ...safeErrorContext(error),
9029
+ code: "serve_request_failed",
9030
+ method: request.method,
9031
+ route: request.routeOptions.url
9032
+ }, "Serve request failed");
9033
+ return reply.code(statusCode).send({
9034
+ code,
9035
+ message
9036
+ });
9037
+ });
9038
+ return app;
9039
+ }
9040
+ function registerPairingRoutes(app, pairing) {
9041
+ app.post("/v1/pairings", async (request, reply) => {
9042
+ const origin = requireOriginHeader(request.headers);
9043
+ return reply.code(201).send(pairing.start(origin));
9044
+ });
9045
+ app.get("/pairings/:pairingId", async (request, reply) => {
9046
+ assertNavigationRequest(request.headers);
9047
+ const { pairingId } = request.params;
9048
+ const approval = pairing.approval(pairingId);
9049
+ return reply.type("text/html; charset=utf-8").send(renderPairingApprovalPage({
9050
+ pairingId,
9051
+ origin: approval.origin,
9052
+ confirmToken: approval.confirmToken
9053
+ }));
9054
+ });
9055
+ app.post("/pairings/:pairingId/confirm", async (request, reply) => {
9056
+ rejectExplicitCrossSite(request.headers);
9057
+ const { pairingId } = request.params;
9058
+ if (!(request.body instanceof URLSearchParams)) throw new ServeHttpError(400, "invalid_body", "Confirmation form is invalid");
9059
+ const { origin } = pairing.confirm(pairingId, request.body.get("confirmToken") ?? "");
9060
+ return reply.type("text/html; charset=utf-8").send(renderPairingResultPage({
9061
+ title: "Connection approved",
9062
+ message: `${origin} can now manage local MoltNet agents on this machine.`
9063
+ }));
9064
+ });
9065
+ app.post("/v1/pairings/:pairingId/claim", async (request) => {
9066
+ const origin = requireOriginHeader(request.headers);
9067
+ const { pairingId } = request.params;
9068
+ return pairing.claim(pairingId, origin);
9069
+ });
9070
+ }
9071
+ function registerStatusRoute(app, options, requirePairedOrigin) {
9072
+ const { store, runs } = options;
9073
+ app.get("/v1/status", async (request) => {
9074
+ requirePairedOrigin(request);
9075
+ return {
9076
+ version: options.version,
9077
+ platform: process.platform,
9078
+ subscriptions: options.subscriptions.list(),
9079
+ agents: store.listActivations().map((activation) => publicAgentView(store, activation)),
9080
+ providers: Object.fromEntries(Object.entries(store.readProviders()).map(([id, provider]) => [id, providerView(provider)])),
9081
+ runs: runViews(runs)
9082
+ };
9083
+ });
9084
+ }
9085
+ function registerAgentRoutes(app, options, requirePairedOrigin) {
9086
+ const { store } = options;
9087
+ app.get("/v1/agents", async (request) => {
9088
+ requirePairedOrigin(request);
9089
+ return store.listActivations().map((activation) => publicAgentView(store, activation));
9090
+ });
9091
+ app.post("/v1/agents", async (request, reply) => {
9092
+ requirePairedOrigin(request);
9093
+ const body = requireBody(request);
9094
+ const signal = requestOperationSignal(request, options.shutdownSignal);
9095
+ const kind = requireString(body, "kind");
9096
+ if (kind === "managed") {
9097
+ if (body["apiUrl"] !== void 0) throw new ServeHttpError(400, "invalid_body", "managed agent registration uses the configured MoltNet API URL; apiUrl cannot be overridden");
9098
+ const entry = await createManagedAgent(store, options.secrets, {
9099
+ name: requireString(body, "name"),
9100
+ apiUrl: options.defaultApiUrl,
9101
+ enrollmentToken: requireString(body, "enrollmentToken"),
9102
+ signal
9103
+ });
9104
+ return reply.code(201).send(publicAgentView(store, entry.activation));
9105
+ }
9106
+ if (kind === "external") {
9107
+ const apiUrl = optionalString(body, "apiUrl");
9108
+ const entry = await attachExternalAgent(store, options.externalSecretProviders, {
9109
+ name: requireString(body, "name"),
9110
+ configDir: requireString(body, "configDir"),
9111
+ ...apiUrl ? { apiUrl } : {},
9112
+ signal
9113
+ });
9114
+ return reply.code(201).send(publicAgentView(store, entry.activation));
9115
+ }
9116
+ throw new ServeHttpError(400, "invalid_body", "\"kind\" must be \"managed\" or \"external\"");
9117
+ });
9118
+ app.post("/v1/agents/:agentName/reconcile", async (request) => {
9119
+ requirePairedOrigin(request);
9120
+ const { agentName } = request.params;
9121
+ const action = requireString(requireBody(request), "action");
9122
+ if (action !== "resume" && action !== "abandon") throw new ServeHttpError(400, "invalid_body", "\"action\" must be \"resume\" or \"abandon\"");
9123
+ const reconciled = await reconcileManagedRegistration(store, options.secrets, agentName, action, void 0, requestOperationSignal(request, options.shutdownSignal));
9124
+ return reconciled ? publicAgentView(store, reconciled.activation) : { abandoned: true };
9125
+ });
9126
+ }
9127
+ function registerProviderRoutes(app, options, requirePairedOrigin) {
9128
+ const { store } = options;
9129
+ let mutationQueue = Promise.resolve();
9130
+ const serialize = (mutation) => {
9131
+ const result = mutationQueue.then(mutation, mutation);
9132
+ mutationQueue = result.then(() => void 0, () => void 0);
9133
+ return result;
9134
+ };
9135
+ app.get("/v1/providers", async (request) => {
9136
+ requirePairedOrigin(request);
9137
+ return Object.fromEntries(Object.entries(store.readProviders()).map(([id, provider]) => [id, providerView(provider)]));
9138
+ });
9139
+ app.post("/v1/providers/:providerId/discover-models", async (request) => {
9140
+ requirePairedOrigin(request);
9141
+ const { providerId: rawProviderId } = request.params;
9142
+ const providerId = assertProviderId(rawProviderId);
9143
+ const provider = store.readProviders()[providerId];
9144
+ if (!provider) throw new ServeHttpError(404, "provider_not_found", `provider "${providerId}" was not found`);
9145
+ const parsed = parseProviderBaseUrl(provider.baseUrl, providerId);
9146
+ const baseUrl = parsed.href.replace(/\/$/u, "");
9147
+ let apiKey;
9148
+ if (provider.apiKeyRef) try {
9149
+ apiKey = await options.secretProviders.resolve(parseSecretReferenceString(provider.apiKeyRef));
9150
+ } catch (error) {
9151
+ request.log.warn({
9152
+ ...safeErrorContext(error),
9153
+ code: "serve_provider_secret_unavailable",
9154
+ providerId
9155
+ }, "Provider API key could not be resolved for model discovery");
9156
+ throw new ServeHttpError(400, "provider_secret_unavailable", `provider "${providerId}" API key could not be resolved`);
9157
+ }
9158
+ const headers = apiKey ? { authorization: `Bearer ${apiKey}` } : {};
9159
+ const fetchImpl = options.discoverFetch ?? fetch;
9160
+ const failures = [];
9161
+ const collector = new ModelDiscoveryCollector();
9162
+ const tryJson = async (endpoint, url) => {
9163
+ let response;
9164
+ try {
9165
+ response = await fetchImpl(url, {
9166
+ headers,
9167
+ redirect: "error",
9168
+ signal: AbortSignal.timeout(1e4)
9169
+ });
9170
+ } catch (error) {
9171
+ const errorType = error instanceof Error ? error.name : typeof error;
9172
+ failures.push({
9173
+ kind: "network",
9174
+ errorType
9175
+ });
9176
+ request.log.warn({
9177
+ code: "serve_provider_discovery_request_failed",
9178
+ endpoint,
9179
+ errorType,
9180
+ providerId
9181
+ }, "Provider model discovery request failed");
9182
+ return null;
9183
+ }
9184
+ if (!response.ok) {
9185
+ failures.push({
9186
+ kind: "http",
9187
+ status: response.status
9188
+ });
9189
+ const context = {
9190
+ code: "serve_provider_discovery_upstream_error",
9191
+ endpoint,
9192
+ providerId,
9193
+ statusCode: response.status
9194
+ };
9195
+ if (response.status >= 500 || response.status === 401 || response.status === 403) request.log.warn(context, "Provider model discovery was rejected");
9196
+ else request.log.info(context, "Provider model discovery endpoint unavailable");
9197
+ return null;
9198
+ }
9199
+ try {
9200
+ return await response.json();
9201
+ } catch {
9202
+ failures.push({ kind: "invalid_response" });
9203
+ request.log.warn({
9204
+ code: "serve_provider_discovery_invalid_json",
9205
+ endpoint,
9206
+ providerId
9207
+ }, "Provider model discovery returned invalid JSON");
9208
+ return null;
9209
+ }
9210
+ };
9211
+ collector.addOpenAiResponse(await tryJson("openai_models", `${baseUrl}/models`));
9212
+ if (collector.size === 0) collector.addOllamaResponse(await tryJson("ollama_tags", `${parsed.origin}/api/tags`));
9213
+ const result = collector.result(providerId, failures);
9214
+ if (result.discoveredCount > result.models.length) request.log.warn({
9215
+ code: "serve_provider_discovery_truncated",
9216
+ discoveredCount: result.discoveredCount,
9217
+ providerId,
9218
+ returnedCount: 500
9219
+ }, "Provider model discovery result was truncated");
9220
+ request.log.info({
9221
+ code: "serve_provider_discovery_completed",
9222
+ modelCount: result.models.length,
9223
+ providerId
9224
+ }, "Provider model discovery completed");
9225
+ return { models: result.models };
9226
+ });
9227
+ app.put("/v1/providers/:providerId", async (request, reply) => {
9228
+ requirePairedOrigin(request);
9229
+ const { providerId: rawProviderId } = request.params;
9230
+ const providerId = assertProviderId(rawProviderId);
9231
+ const body = requireBody(request);
9232
+ const baseUrl = requireString(body, "baseUrl");
9233
+ parseProviderBaseUrl(baseUrl, providerId);
9234
+ const entry = {
9235
+ api: requireString(body, "api"),
9236
+ baseUrl,
9237
+ envName: assertProviderEnvName(providerId, requireString(body, "envName")),
9238
+ models: stringArray(body, "models", { allowEmpty: true })
9239
+ };
9240
+ const apiKey = optionalString(body, "apiKey");
9241
+ await serialize(async () => {
9242
+ const providers = store.readProviders();
9243
+ if (apiKey) {
9244
+ const key = `pi-provider/${providerId}`;
9245
+ await options.secrets.write(key, apiKey);
9246
+ entry.apiKeyRef = formatSecretReferenceString({
9247
+ provider: FILE_SECRET_PROVIDER,
9248
+ key
9249
+ });
9250
+ } else if (providers[providerId]?.apiKeyRef && providers[providerId].baseUrl === entry.baseUrl) entry.apiKeyRef = providers[providerId].apiKeyRef;
9251
+ providers[providerId] = entry;
9252
+ store.writeProviders(providers);
9253
+ });
9254
+ return reply.code(200).send(providerView(entry));
9255
+ });
9256
+ }
9257
+ function runViews(runs) {
9258
+ return runs.list(RUN_HISTORY_LIMIT).map((record) => ({
9259
+ ...record,
9260
+ active: runs.isActive(record.id)
9261
+ }));
9262
+ }
9263
+ function registerSubscriptionRoutes(app, options, requirePairedOrigin) {
9264
+ app.get("/v1/subscriptions", async (request) => {
9265
+ requirePairedOrigin(request);
9266
+ return options.subscriptions.list();
9267
+ });
9268
+ app.post("/v1/subscriptions/:providerId/login", async (request, reply) => {
9269
+ requirePairedOrigin(request);
9270
+ const { providerId } = request.params;
9271
+ const login = await options.subscriptions.start(providerId);
9272
+ return reply.code(201).send(login);
9273
+ });
9274
+ app.get("/v1/subscriptions/:providerId/login", async (request) => {
9275
+ requirePairedOrigin(request);
9276
+ const { providerId } = request.params;
9277
+ return options.subscriptions.status(providerId);
9278
+ });
9279
+ app.delete("/v1/subscriptions/:providerId/login", async (request) => {
9280
+ requirePairedOrigin(request);
9281
+ const { providerId } = request.params;
9282
+ return options.subscriptions.cancel(providerId);
9283
+ });
9284
+ }
9285
+ function registerRunRoutes(app, options, requirePairedOrigin) {
9286
+ const { runs } = options;
9287
+ app.get("/v1/runs", async (request) => {
9288
+ requirePairedOrigin(request);
9289
+ return runViews(runs);
9290
+ });
9291
+ app.post("/v1/runs", async (request, reply) => {
9292
+ requirePairedOrigin(request);
9293
+ const body = requireBody(request);
9294
+ const record = await runs.start({
9295
+ agent: requireString(body, "agent"),
9296
+ teamId: requireString(body, "teamId"),
9297
+ profiles: stringArray(body, "profiles"),
9298
+ taskTypes: stringArray(body, "taskTypes"),
9299
+ mode: requireString(body, "mode")
9300
+ }, requestOperationSignal(request, options.shutdownSignal));
9301
+ return reply.code(201).send(record);
9302
+ });
9303
+ app.delete("/v1/runs/:runId", async (request) => {
9304
+ requirePairedOrigin(request);
9305
+ const { runId } = request.params;
9306
+ return runs.stop(runId);
9307
+ });
9308
+ registerRunLogRoute(app, options, requirePairedOrigin);
9309
+ }
9310
+ function registerRunLogRoute(app, options, requirePairedOrigin) {
9311
+ const { runs, store } = options;
9312
+ let openStreams = 0;
9313
+ app.get("/v1/runs/:runId/logs", async (request, reply) => {
9314
+ requirePairedOrigin(request);
9315
+ const { runId } = request.params;
9316
+ const record = runs.status(runId);
9317
+ store.resolveRunLogPath(record.id);
9318
+ if (openStreams >= MAX_LOG_STREAMS) throw new ServeHttpError(429, "rate_limited", "Too many concurrent log streams");
9319
+ openStreams += 1;
9320
+ reply.raw.writeHead(200, {
9321
+ "content-type": "text/event-stream",
9322
+ "cache-control": "no-store",
9323
+ connection: "keep-alive",
9324
+ ...corsHeadersFor(request, options)
9325
+ });
9326
+ const readState = {
9327
+ offset: 0,
9328
+ fragment: ""
9329
+ };
9330
+ let closed = false;
9331
+ let pollTimer;
9332
+ const durationTimer = setTimeout(() => finish(), LOG_STREAM_MAX_DURATION_MS);
9333
+ durationTimer.unref();
9334
+ function finish(destroy = false) {
9335
+ if (closed) return;
9336
+ closed = true;
9337
+ openStreams -= 1;
9338
+ if (pollTimer) clearTimeout(pollTimer);
9339
+ clearTimeout(durationTimer);
9340
+ if (destroy) reply.raw.destroy();
9341
+ else reply.raw.end();
9342
+ }
9343
+ const writeData = async (line) => {
9344
+ if (closed || reply.raw.write(`data: ${line}\n\n`)) return;
9345
+ await new Promise((resolvePromise) => {
9346
+ const done = () => {
9347
+ reply.raw.off("drain", done);
9348
+ reply.raw.off("close", done);
9349
+ resolvePromise();
9350
+ };
9351
+ reply.raw.once("drain", done);
9352
+ reply.raw.once("close", done);
9353
+ });
9354
+ };
9355
+ const push = async () => {
9356
+ const handle = await open(store.resolveRunLogPath(record.id), constants.O_RDONLY | constants.O_NOFOLLOW);
9357
+ try {
9358
+ const { lines, omitted } = await readServeLogDelta(handle, readState);
9359
+ if (omitted) await writeData("[older log output omitted]");
9360
+ for (const line of lines) await writeData(line);
9361
+ } finally {
9362
+ await handle.close();
9363
+ }
9364
+ };
9365
+ const fail = (error) => {
9366
+ if (closed) return;
9367
+ request.log.warn({
9368
+ ...safeErrorContext(error),
9369
+ code: "serve_log_tail_failed",
9370
+ runId
9371
+ }, "serve log tail failed");
9372
+ finish(true);
9373
+ };
9374
+ const schedule = () => {
9375
+ if (closed) return;
9376
+ pollTimer = setTimeout(() => {
9377
+ push().then(() => {
9378
+ if (runs.isActive(record.id)) schedule();
9379
+ else finish();
9380
+ }).catch(fail);
9381
+ }, LOG_POLL_INTERVAL_MS);
9382
+ pollTimer.unref();
9383
+ };
9384
+ request.raw.on("close", () => finish());
9385
+ try {
9386
+ await push();
9387
+ if (runs.isActive(record.id)) schedule();
9388
+ else finish();
9389
+ } catch (error) {
9390
+ fail(error);
9391
+ }
9392
+ return reply;
9393
+ });
9394
+ }
9395
+ function safeErrorContext(error) {
9396
+ const context = { errorType: error instanceof Error ? error.name : typeof error };
9397
+ const applicationCode = safeErrorToken(error?.code);
9398
+ if (applicationCode) context["applicationCode"] = applicationCode;
9399
+ const cause = error instanceof Error ? error.cause : void 0;
9400
+ const fsCode = safeErrorToken(cause?.code);
9401
+ const syscall = safeErrorToken(cause?.syscall);
9402
+ if (fsCode) context["fsCode"] = fsCode;
9403
+ if (syscall) context["syscall"] = syscall;
9404
+ return context;
9405
+ }
9406
+ function safeErrorToken(value) {
9407
+ return typeof value === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(value) ? value : void 0;
9408
+ }
9409
+ function providerView(provider) {
9410
+ return {
9411
+ api: provider.api,
9412
+ baseUrl: provider.baseUrl,
9413
+ envName: provider.envName,
9414
+ models: provider.models,
9415
+ hasApiKey: Boolean(provider.apiKeyRef)
9416
+ };
9417
+ }
9418
+ function corsHeadersFor(request, options) {
9419
+ const origin = request.headers.origin;
9420
+ if (isConfiguredOrigin(origin, options)) return {
9421
+ "access-control-allow-origin": origin,
9422
+ vary: "origin"
9423
+ };
9424
+ return {};
9425
+ }
9426
+ function isConfiguredOrigin(origin, options) {
9427
+ return typeof origin === "string" && (options.allowedOrigins.includes(origin) || origin === options.selfOrigin);
9428
+ }
9429
+ function normalizeServeError(error) {
9430
+ if (error instanceof ServeHttpError) return {
9431
+ statusCode: error.statusCode,
9432
+ code: error.code,
9433
+ message: error.message
9434
+ };
9435
+ if (isLoopbackViolation(error)) return {
9436
+ statusCode: error.kind === "origin_required" || error.kind === "origin_invalid" || error.kind === "origin_not_allowed" ? 403 : 400,
9437
+ code: error.kind,
9438
+ message: error.message
9439
+ };
9440
+ if (error instanceof ServePairingError) return {
9441
+ statusCode: error.code === "pairing_not_found" ? 404 : error.code === "pairing_token_invalid" || error.code === "pairing_not_approved" ? 401 : 403,
9442
+ code: error.code,
9443
+ message: error.message
9444
+ };
9445
+ if (error instanceof ServeStoreError) return {
9446
+ statusCode: error.code === "not_found" ? 404 : error.code === "io_error" ? 500 : 400,
9447
+ code: error.code,
9448
+ message: error.message
9449
+ };
9450
+ if (error instanceof ServeRunError) return {
9451
+ statusCode: error.code === "run_not_found" ? 404 : 400,
9452
+ code: error.code,
9453
+ message: error.message
9454
+ };
9455
+ if (error instanceof ServeSubscriptionError) return {
9456
+ statusCode: error.code === "login_not_found" ? 404 : error.code === "provider_unknown" ? 404 : 400,
9457
+ code: error.code,
9458
+ message: error.message
9459
+ };
9460
+ if (error instanceof ServeModelDiscoveryError) return {
9461
+ statusCode: error.statusCode,
9462
+ code: error.code,
9463
+ message: error.message
9464
+ };
9465
+ if (error instanceof ServeIdentityError) return {
9466
+ statusCode: error.code === "agent_exists" ? 409 : 400,
9467
+ code: error.code,
9468
+ message: error.message
9469
+ };
9470
+ return {
9471
+ statusCode: 500,
9472
+ code: "internal_error",
9473
+ message: "The local supervisor could not complete the request."
9474
+ };
9475
+ }
9476
+ //#endregion
9477
+ //#region src/cli/serve.ts
9478
+ /**
9479
+ * `moltnet-agent serve` — per-user loopback supervisor (#2061).
9480
+ *
9481
+ * Starts nothing on its own: it binds 127.0.0.1 and waits for a paired
9482
+ * Console origin to configure agents/providers and start/stop runs.
9483
+ */
9484
+ var DEFAULT_PORT = 17374;
9485
+ var DEFAULT_ALLOWED_ORIGINS = "https://console.themolt.net";
9486
+ var DEFAULT_API_URL = "https://api.themolt.net";
9487
+ var SHUTDOWN_TIMEOUT_MS = 15e3;
9488
+ async function runServe(argv) {
9489
+ if (isHelpFlag(argv)) {
9490
+ console.log(SERVE_HELP);
9491
+ return 0;
9492
+ }
9493
+ const envConfig = loadServeEnvConfig();
9494
+ const { values } = parseArgs({
9495
+ args: argv,
9496
+ options: {
9497
+ port: { type: "string" },
9498
+ "allowed-origins": { type: "string" },
9499
+ root: { type: "string" },
9500
+ "api-url": { type: "string" }
9501
+ }
9502
+ });
9503
+ const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
9504
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
9505
+ console.error(`Invalid --port: ${String(values.port)}`);
9506
+ return 1;
9507
+ }
9508
+ const allowedOrigins = parseAllowedOrigins(values["allowed-origins"] ?? (envConfig.allowedOrigins || DEFAULT_ALLOWED_ORIGINS));
9509
+ const root = values.root ?? resolveServeRoot({
9510
+ root: envConfig.root,
9511
+ xdgConfigHome: envConfig.xdgConfigHome
9512
+ });
9513
+ const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
9514
+ const store = new ServeStore(root).ensure();
9515
+ const { logger, shutdown: shutdownLogger } = createRootLogger({
9516
+ name: "agent-daemon.serve",
9517
+ level: envConfig.logLevel || "info"
9518
+ });
9519
+ try {
9520
+ try {
9521
+ return await withServeLock(root, async () => {
9522
+ const secrets = new FileSecretProvider({
9523
+ root: store.secretsDir,
9524
+ writable: true
9525
+ });
9526
+ const secretProviders = createNodeSecretProviderRegistry().register(secrets);
9527
+ const externalSecretProviders = createNodeSecretProviderRegistry();
9528
+ const pairing = new PairingService();
9529
+ const shutdownController = new AbortController();
9530
+ const subscriptions = new ProviderLoginService({
9531
+ authPath: store.piAuthJsonPath,
9532
+ logger
9533
+ });
9534
+ const runs = new RunManager({
9535
+ store,
9536
+ secretProviders,
9537
+ externalSecretProviders,
9538
+ baseEnv: processEnvSnapshot(),
9539
+ logger
9540
+ });
9541
+ const app = buildServeServer({
9542
+ store,
9543
+ secrets,
9544
+ secretProviders,
9545
+ externalSecretProviders,
9546
+ pairing,
9547
+ runs,
9548
+ subscriptions,
9549
+ allowedOrigins,
9550
+ selfOrigin: `http://127.0.0.1:${port}`,
9551
+ defaultApiUrl,
9552
+ version: "dev",
9553
+ logger,
9554
+ shutdownSignal: shutdownController.signal
9555
+ });
9556
+ try {
9557
+ const address = await app.listen({
9558
+ host: "127.0.0.1",
9559
+ port
9560
+ });
9561
+ console.error(`moltnet-agent serve listening on ${address}`);
9562
+ console.error(`config root: ${root}`);
9563
+ console.error(`allowed origins: ${allowedOrigins.join(", ")}`);
9564
+ console.error("Pair from the Console \"Local runtime\" page; approve the one-click prompt this server opens.");
9565
+ return await waitForServeShutdown(runs, app, shutdownController);
9566
+ } catch (cause) {
9567
+ await app.close().catch(() => void 0);
9568
+ throw cause;
9569
+ }
9570
+ }, { onCompromised: (error) => {
9571
+ console.error(error.message);
9572
+ process.exitCode = 1;
9573
+ process.kill(process.pid, "SIGTERM");
9574
+ } });
9575
+ } catch (cause) {
9576
+ if (cause instanceof ServeLockError) {
9577
+ console.error(cause.message);
9578
+ return 1;
9579
+ }
9580
+ throw cause;
9581
+ }
9582
+ } finally {
9583
+ await shutdownLogger();
9584
+ }
9585
+ }
9586
+ function waitForServeShutdown(runs, app, shutdownController) {
9587
+ return new Promise((resolvePromise) => {
9588
+ let shuttingDown = false;
9589
+ const shutdown = () => {
9590
+ if (shuttingDown) return;
9591
+ shuttingDown = true;
9592
+ shutdownController.abort();
9593
+ (async () => {
9594
+ app.server.closeAllConnections();
9595
+ const cleanupPromise = Promise.allSettled([runs.stopAll(), app.close()]);
9596
+ let timedOut = false;
9597
+ let deadlineTimer;
9598
+ await Promise.race([cleanupPromise, new Promise((resolveDeadline) => {
9599
+ deadlineTimer = setTimeout(() => {
9600
+ timedOut = true;
9601
+ resolveDeadline();
9602
+ }, SHUTDOWN_TIMEOUT_MS);
9603
+ })]);
9604
+ if (deadlineTimer) clearTimeout(deadlineTimer);
9605
+ let forcedExitTimer;
9606
+ if (timedOut) {
9607
+ console.error("shutdown cleanup exceeded its 15 second deadline; force-stopping runs");
9608
+ runs.forceStopAll();
9609
+ app.server.closeAllConnections();
9610
+ forcedExitTimer = setTimeout(() => process.exit(1), 2e3);
9611
+ }
9612
+ const results = await cleanupPromise;
9613
+ if (forcedExitTimer) clearTimeout(forcedExitTimer);
9614
+ const failures = results.filter((result) => result.status === "rejected");
9615
+ for (const failure of failures) console.error(`shutdown cleanup failed: ${failure.reason.message}`);
9616
+ handlers.dispose();
9617
+ const exitCode = typeof process.exitCode === "number" ? process.exitCode : 0;
9618
+ resolvePromise(failures.length > 0 ? 1 : exitCode);
9619
+ })();
9620
+ };
9621
+ const handlers = installShutdownSignalHandlers({
9622
+ logDrain: () => console.error("shutting down: stopping runs…"),
9623
+ drain: shutdown
9624
+ });
9625
+ });
9626
+ }
9627
+ //#endregion
6857
9628
  //#region src/lib/runtime-session-sync.ts
6858
9629
  async function syncRuntimeSessions(deps, input) {
6859
9630
  const result = {
@@ -7037,6 +9808,7 @@ async function runAgentDaemonCli(options) {
7037
9808
  case "poll": return runPoll(rest, options.runtime);
7038
9809
  case "once": return runOnce(rest, options.runtime);
7039
9810
  case "drain": return runDrain(rest, options.runtime);
9811
+ case "serve": return runServe(rest);
7040
9812
  case "sync-sessions": return runSyncSessions(rest);
7041
9813
  case "-h":
7042
9814
  case "--help":