@themoltnet/agent-daemon 0.50.0 → 0.52.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/README.md CHANGED
@@ -100,7 +100,7 @@ environment only — never store it in `moltnet.json`. The daemon reconciles a
100
100
  team-bound key against `--team` at startup; an identity-scoped key may select
101
101
  any team where the agent is authorized. It fails fast if the key is rejected,
102
102
  is not an agent, or a team binding mismatches. See
103
- [Run the daemon with an agent key](../../docs/operate/running-agents.md#run-the-daemon-with-an-agent-key).
103
+ [Run the daemon with an agent key](../../docs/operate/agent-keys.md#run-the-daemon-with-an-agent-key).
104
104
 
105
105
  Daemon authentication and the guest boundary are two separate concerns. How the
106
106
  daemon authenticates (an agent key, or OAuth2 resolved from
@@ -196,7 +196,7 @@ host-exec command is safe to run without a dialog.
196
196
  ### Remote runtime profiles
197
197
 
198
198
  The canonical user-facing guide lives in the public docs:
199
- [Running Agents § Runtime Profiles](https://docs.themolt.net/operate/running-agents#runtime-profiles).
199
+ [Running Agents § Runtime Profiles](https://docs.themolt.net/operate/runtime-profiles).
200
200
 
201
201
  ## Correlation anchors
202
202
 
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEzD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,OAAO,EAAE,oBAAoB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBlB"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,OAAO,EAAE,oBAAoB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2ClB"}
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as cryptoService, defaultPiDaemonAdapter, i as compileExecutionPlan, n as createExecutionPlanSnapshot, r as parseCredentialRequirements, t as runtimeExecutionOffer } from "./pi.js";
3
3
  import { assertRuntimeAdapterSupportsProfile } from "./runtime.js";
4
+ import { createRequire } from "node:module";
5
+ import { pino, transport } from "pino";
4
6
  import { Type } from "typebox";
5
7
  import "multiformats/cid";
6
8
  import "multiformats/codecs/json";
@@ -14,9 +16,8 @@ import { FILE_SECRET_PROVIDER, FileSecretProvider, connect, createNodeSecretProv
14
16
  import { execFile, execFileSync, spawn } from "node:child_process";
15
17
  import { constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
16
18
  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";
19
+ import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual, webcrypto } from "node:crypto";
18
20
  import { once } from "node:events";
19
- import { pino, transport } from "pino";
20
21
  import { metrics } from "@opentelemetry/api";
21
22
  import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
22
23
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
@@ -26,19 +27,23 @@ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
26
27
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
27
28
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
28
29
  import { AsyncLocalStorage } from "node:async_hooks";
29
- import { mkdir, open, realpath, stat } from "node:fs/promises";
30
+ import { mkdir, open, readFile, realpath, stat, writeFile } from "node:fs/promises";
30
31
  import { pipeline } from "node:stream/promises";
32
+ import { createInterface } from "node:readline/promises";
31
33
  import cors from "@fastify/cors";
32
34
  import helmet from "@fastify/helmet";
33
35
  import { lock, lockSync } from "proper-lockfile";
34
36
  import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
35
37
  import { Transform, Writable } from "node:stream";
36
38
  import { writePiConfig } from "@themoltnet/pi-runtime/pi-config";
37
- import { homedir } from "node:os";
39
+ import { homedir, platform } from "node:os";
40
+ import { pathToFileURL } from "node:url";
38
41
  import { StringDecoder } from "node:string_decoder";
39
42
  import rateLimit from "@fastify/rate-limit";
40
43
  import Fastify from "fastify";
41
44
  import { isIP } from "node:net";
45
+ import "reflect-metadata";
46
+ import { BasicConstraintsExtension, ExtendedKeyUsage, ExtendedKeyUsageExtension, IP, KeyUsageFlags, KeyUsagesExtension, SubjectAlternativeNameExtension, X509CertificateGenerator } from "@peculiar/x509";
42
47
  import { createGzip } from "node:zlib";
43
48
  //#region ../../libs/tasks/src/rubric.ts
44
49
  /**
@@ -3429,8 +3434,12 @@ Commands:
3429
3434
  server Loopback supervisor for console-managed runs: pairing,
3430
3435
  agent/provider config store, and start/stop of poll/drain
3431
3436
  child processes. Binds 127.0.0.1 only.
3437
+ server trust
3438
+ Install the per-user macOS local-HTTPS CA after explicit consent.
3432
3439
  sync-sessions
3433
3440
  Repair durable runtime-session checkpoints from local slot files.
3441
+ update check
3442
+ Check the stable MoltNet agent release without reading credentials.
3434
3443
 
3435
3444
  Run \`agent-daemon <command> --help\` for command-specific flags.
3436
3445
 
@@ -3578,6 +3587,10 @@ Options:
3578
3587
  (or MOLTNET_AGENT_SERVER_ROOT).
3579
3588
  --api-url <url> Default MoltNet API for new managed agents.
3580
3589
  Default: https://api.themolt.net.
3590
+
3591
+ On macOS, the first interactive run asks to trust a per-user local CA in the
3592
+ login keychain and serves HTTPS. Run \`agent-daemon server trust --remove\` to
3593
+ remove that exact CA. Linux continues to use the Chromium PNA HTTP path.
3581
3594
  `;
3582
3595
  //#endregion
3583
3596
  //#region src/lib/identity-pin.ts
@@ -3798,6 +3811,12 @@ function loadAgentServerEnvConfig() {
3798
3811
  function processEnvSnapshot() {
3799
3812
  return process.env;
3800
3813
  }
3814
+ function loadUpdateEnvConfig() {
3815
+ return {
3816
+ xdgCacheHome: process.env["XDG_CACHE_HOME"] ?? "",
3817
+ localAppData: process.env["LOCALAPPDATA"] ?? ""
3818
+ };
3819
+ }
3801
3820
  //#endregion
3802
3821
  //#region src/lib/abort-active-attempt.ts
3803
3822
  /** Best-effort signal cleanup; lease expiry remains the final backstop. */
@@ -3835,7 +3854,7 @@ async function resolveDaemonAgentIdentity(input) {
3835
3854
  }
3836
3855
  //#endregion
3837
3856
  //#region src/lib/correlation.ts
3838
- var execFileAsync = promisify(execFile);
3857
+ var execFileAsync$1 = promisify(execFile);
3839
3858
  var CORRELATION_TRAILER_KEY = "Moltnet-Correlation-Id";
3840
3859
  var CORRELATION_MARKER_RE = /<!--\s*moltnet-correlation:\s*([\w-]+)\s*-->/i;
3841
3860
  new RegExp(`^${CORRELATION_TRAILER_KEY}:\\s*(\\S+)\\s*$`, "m");
@@ -3882,7 +3901,7 @@ function makePrBodyAnchorWriter(deps) {
3882
3901
  function createGhCliClient() {
3883
3902
  return {
3884
3903
  async get({ owner, repo, number }) {
3885
- const { stdout } = await execFileAsync("gh", [
3904
+ const { stdout } = await execFileAsync$1("gh", [
3886
3905
  "api",
3887
3906
  `repos/${owner}/${repo}/pulls/${number}`,
3888
3907
  "--jq",
@@ -3891,7 +3910,7 @@ function createGhCliClient() {
3891
3910
  return JSON.parse(stdout);
3892
3911
  },
3893
3912
  async patch({ owner, repo, number }, body) {
3894
- await execFileAsync("gh", [
3913
+ await execFileAsync$1("gh", [
3895
3914
  "api",
3896
3915
  "-X",
3897
3916
  "PATCH",
@@ -7356,6 +7375,12 @@ function renderPairingApprovalPage(input) {
7356
7375
  </style>
7357
7376
  </head>
7358
7377
  <body>
7378
+ <script>
7379
+ // The Console must remain this popup's opener until it finishes navigating
7380
+ // from about:blank. Safari rejects that cross-origin navigation otherwise.
7381
+ // Once this trusted local approval document has loaded, it needs no opener.
7382
+ window.opener = null;
7383
+ <\/script>
7359
7384
  <main>
7360
7385
  <h1>Allow this site to manage local MoltNet agents?</h1>
7361
7386
  <p><code>${escapeHtml(input.origin)}</code> asks to configure agents and start or stop local daemon runs on this machine.</p>
@@ -8138,7 +8163,7 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
8138
8163
  } catch (cause) {
8139
8164
  if (!registeredIdentityId && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
8140
8165
  store.clearPendingRegistration(alias);
8141
- throw new AgentServerIdentityError("registration_failed", `registration for "${alias}" was rejected`, { cause });
8166
+ throw new AgentServerIdentityError("registration_failed", registrationRejectionMessage(alias, cause), { cause });
8142
8167
  }
8143
8168
  if (store.hasPendingRegistration(alias)) throw new AgentServerIdentityError("registration_incomplete", registeredIdentityId ? `identity "${registeredIdentityId}" was registered but local activation is incomplete; reconcile or clear its pending Agent Server record before retrying` : `registration for "${alias}" may be incomplete; inspect the remote API before changing its pending Agent Server record`, { cause });
8144
8169
  throw cause;
@@ -8146,6 +8171,9 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
8146
8171
  releaseAlias();
8147
8172
  }
8148
8173
  }
8174
+ function registrationRejectionMessage(alias, cause) {
8175
+ return `registration for "${alias}" was rejected${cause.statusCode === void 0 ? "" : ` (${cause.statusCode})`}: ${cause.detail?.trim() || cause.message}`;
8176
+ }
8149
8177
  /** Resume a fully persisted registration or explicitly abandon local recovery. */
8150
8178
  async function reconcileManagedRegistration(store, secrets, aliasInput, action, connectAgent = connect, signal) {
8151
8179
  const alias = assertStoreName("agent name", aliasInput);
@@ -8475,7 +8503,7 @@ var RunManager = class {
8475
8503
  };
8476
8504
  }
8477
8505
  /** Assemble child env + args for a run. Exposed for tests. */
8478
- async prepare(spec, agent, piDir, providers = this.store.readProviders()) {
8506
+ async prepare(spec, agent, piDir, providers = this.store.readProviders(), runtimeModule) {
8479
8507
  const { activation, config } = agent;
8480
8508
  const homeDir = join(dirname(piDir), "home");
8481
8509
  const env = {
@@ -8499,6 +8527,7 @@ var RunManager = class {
8499
8527
  };
8500
8528
  })();
8501
8529
  const args = [
8530
+ ...runtimeModule ? ["--runtime", runtimeModule] : [],
8502
8531
  spec.mode,
8503
8532
  "--agent",
8504
8533
  target.agentName,
@@ -8569,7 +8598,8 @@ var RunManager = class {
8569
8598
  const runDir = this.store.runDir(id);
8570
8599
  const piDir = join(runDir, "pi");
8571
8600
  const providers = this.store.readProviders();
8572
- const { args, env, cwd } = await this.prepare(spec, agent, piDir, providers);
8601
+ const runtimeModule = this.options.resolveRuntimeModule ? await this.options.resolveRuntimeModule(spec, agent, dirname(piDir)) : this.options.runtimeRegistry ? await this.resolveRuntimeModule(spec, agent, dirname(piDir)) : void 0;
8602
+ const { args, env, cwd } = await this.prepare(spec, agent, piDir, providers, runtimeModule);
8573
8603
  this.assertStartOpen(signal);
8574
8604
  let child;
8575
8605
  let logStream;
@@ -8700,6 +8730,46 @@ var RunManager = class {
8700
8730
  throw cause;
8701
8731
  }
8702
8732
  }
8733
+ async resolveRuntimeModule(spec, activated, cwd) {
8734
+ const profiles = await resolveRuntimeProfiles({
8735
+ agent: await this.connectActivatedAgent(activated),
8736
+ profiles: spec.profiles,
8737
+ teamId: spec.teamId,
8738
+ cwd
8739
+ });
8740
+ const kinds = [...new Set(profiles.map((profile) => profile.runtimeKind))];
8741
+ if (kinds.length !== 1) throw new AgentServerRunError("invalid_spec", "All profiles in a server-managed run must use the same runtime kind.");
8742
+ const kind = kinds[0];
8743
+ let registration;
8744
+ try {
8745
+ registration = this.options.runtimeRegistry?.resolve(kind);
8746
+ } catch (cause) {
8747
+ this.log("warn", "registered runtime unavailable locally", {
8748
+ runtimeKind: kind,
8749
+ ...runtimeRegistryFailure(cause)
8750
+ });
8751
+ throw new AgentServerRunError("invalid_spec", `Registered runtime "${kind}" is unavailable locally; re-register it before starting a run.`);
8752
+ }
8753
+ if (registration) return registration.moduleUrl;
8754
+ if (kind === "gondolin_pi") return void 0;
8755
+ throw new AgentServerRunError("invalid_spec", `No local runtime is registered for profile kind "${kind}".`);
8756
+ }
8757
+ async connectActivatedAgent(activated) {
8758
+ const { activation, config } = activated;
8759
+ if (activation.source === "managed") {
8760
+ const agentKey = await resolveAgentKey(config, this.options.secretProviders);
8761
+ if (!agentKey) throw new AgentServerRunError("invalid_spec", `managed agent "${activation.alias}" has no agent key`);
8762
+ return connect({
8763
+ agentKey,
8764
+ apiUrl: activation.apiUrl
8765
+ });
8766
+ }
8767
+ return connect({
8768
+ configDir: dirname(activation.configPath),
8769
+ apiUrl: activation.apiUrl ?? activation.configApiUrl,
8770
+ secretProviders: this.options.externalSecretProviders
8771
+ });
8772
+ }
8703
8773
  stop(id) {
8704
8774
  const record = this.store.readRun(id);
8705
8775
  if (!record) throw new AgentServerRunError("run_not_found", `run "${id}" was not found`);
@@ -8882,6 +8952,14 @@ function safeRunError(error) {
8882
8952
  ...typeof code === "string" ? { errorCode: code } : {}
8883
8953
  };
8884
8954
  }
8955
+ function runtimeRegistryFailure(error) {
8956
+ const code = error?.code;
8957
+ return {
8958
+ errorType: error instanceof Error ? error.name : typeof error,
8959
+ ...typeof code === "string" ? { errorCode: code } : {},
8960
+ ...error instanceof Error ? { errorMessage: error.message.slice(0, 512) } : {}
8961
+ };
8962
+ }
8885
8963
  function sanitizeChildBaseEnv(baseEnv) {
8886
8964
  return Object.fromEntries(Object.entries(baseEnv).filter(([name]) => INHERITED_ENV_NAMES.has(name) || INHERITED_MOLTNET_ENV_NAMES.has(name)));
8887
8965
  }
@@ -8897,6 +8975,168 @@ async function terminateChild(child) {
8897
8975
  child.kill("SIGKILL");
8898
8976
  });
8899
8977
  }
8978
+ //#endregion
8979
+ //#region src/runtime-loader.ts
8980
+ function extractRuntimeModule(argv) {
8981
+ const remaining = [];
8982
+ let specifier;
8983
+ let parsingOptions = true;
8984
+ for (let index = 0; index < argv.length; index += 1) {
8985
+ const argument = argv[index];
8986
+ if (argument === "--") {
8987
+ parsingOptions = false;
8988
+ remaining.push(argument);
8989
+ continue;
8990
+ }
8991
+ if (!parsingOptions) {
8992
+ remaining.push(argument);
8993
+ continue;
8994
+ }
8995
+ let candidate;
8996
+ if (argument === "--runtime") {
8997
+ candidate = argv[index + 1];
8998
+ if (!candidate) throw new Error("Missing value for --runtime");
8999
+ index += 1;
9000
+ } else if (argument.startsWith("--runtime=")) {
9001
+ candidate = argument.slice(10);
9002
+ if (!candidate) throw new Error("Missing value for --runtime");
9003
+ } else {
9004
+ remaining.push(argument);
9005
+ continue;
9006
+ }
9007
+ if (specifier) throw new Error("--runtime may be specified only once");
9008
+ specifier = candidate;
9009
+ }
9010
+ return {
9011
+ argv: remaining,
9012
+ specifier
9013
+ };
9014
+ }
9015
+ function resolveRuntimeModuleUrl(specifier, cwd = process.cwd()) {
9016
+ if (specifier.startsWith("file:")) return new URL(specifier).href;
9017
+ if (isAbsolute(specifier) || specifier.startsWith("./") || specifier.startsWith("../")) return pathToFileURL(resolve(cwd, specifier)).href;
9018
+ return pathToFileURL(createRequire(resolve(cwd, "package.json")).resolve(specifier)).href;
9019
+ }
9020
+ async function loadDaemonRuntimeAdapter(specifier, options = {}) {
9021
+ let moduleUrl;
9022
+ try {
9023
+ moduleUrl = resolveRuntimeModuleUrl(specifier, options.cwd);
9024
+ } catch (cause) {
9025
+ throw new Error(`Unable to resolve runtime module "${specifier}" from ${options.cwd ?? process.cwd()}`, { cause });
9026
+ }
9027
+ let namespace;
9028
+ try {
9029
+ namespace = await import(moduleUrl);
9030
+ } catch (cause) {
9031
+ throw new Error(`Unable to load runtime module "${specifier}"`, { cause });
9032
+ }
9033
+ const adapter = namespace.default;
9034
+ if (!isDaemonRuntimeAdapter(adapter)) throw new Error(`Runtime module "${specifier}" must default-export a DaemonRuntimeAdapter`);
9035
+ return adapter;
9036
+ }
9037
+ function isDaemonRuntimeAdapter(value) {
9038
+ if (!value || typeof value !== "object") return false;
9039
+ const candidate = value;
9040
+ return typeof candidate.runtimeKind === "string" && candidate.runtimeKind.length > 0 && typeof candidate.prepare === "function";
9041
+ }
9042
+ //#endregion
9043
+ //#region src/lib/agent-server/runtime-registry.ts
9044
+ var LOCKFILE_NAMES = [
9045
+ "pnpm-lock.yaml",
9046
+ "package-lock.json",
9047
+ "yarn.lock",
9048
+ "bun.lockb"
9049
+ ];
9050
+ /** Local, operator-owned allowlist for executable daemon runtime modules. */
9051
+ var RuntimeRegistry = class {
9052
+ constructor(root) {
9053
+ this.root = root;
9054
+ }
9055
+ get path() {
9056
+ return join(this.root, "runtime-registry.json");
9057
+ }
9058
+ list() {
9059
+ if (!existsSync(this.path)) return [];
9060
+ const value = JSON.parse(readFileSync(this.path, "utf8"));
9061
+ if (!Array.isArray(value) || !value.every(isRegistration)) throw new AgentServerStoreError("invalid_state", "runtime registry contains an invalid registration");
9062
+ return value;
9063
+ }
9064
+ async register(kind, specifier, cwd = process.cwd()) {
9065
+ kind = assertStoreName("runtime kind", kind);
9066
+ mkdirSync(this.root, {
9067
+ recursive: true,
9068
+ mode: 448
9069
+ });
9070
+ const moduleUrl = resolveRuntimeModuleUrl(specifier, cwd);
9071
+ const adapter = await loadDaemonRuntimeAdapter(specifier, { cwd });
9072
+ if (adapter.runtimeKind !== kind) throw new Error(`Runtime module provides "${adapter.runtimeKind}", not registered kind "${kind}".`);
9073
+ if (!moduleUrl.startsWith("file:")) throw new Error("Runtime registration must resolve to a local file URL.");
9074
+ const entryHash = hashFile(new URL(moduleUrl));
9075
+ const lockfilePath = isPackageSpecifier(specifier) ? findLockfile(cwd) : void 0;
9076
+ if (isPackageSpecifier(specifier) && !lockfilePath) throw new Error("Package runtime registration requires a pnpm, npm, Yarn, or Bun lockfile in the current project or a parent directory.");
9077
+ const entry = {
9078
+ kind,
9079
+ moduleUrl,
9080
+ entryHash,
9081
+ ...lockfilePath ? {
9082
+ lockfilePath,
9083
+ lockfileHash: hashPath(lockfilePath)
9084
+ } : {},
9085
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
9086
+ };
9087
+ const entries = this.list().filter((candidate) => candidate.kind !== kind);
9088
+ entries.push(entry);
9089
+ writeRegistry(this.path, entries);
9090
+ return entry;
9091
+ }
9092
+ unregister(kind) {
9093
+ kind = assertStoreName("runtime kind", kind);
9094
+ const entries = this.list();
9095
+ const next = entries.filter((entry) => entry.kind !== kind);
9096
+ if (next.length === entries.length) return false;
9097
+ writeRegistry(this.path, next);
9098
+ return true;
9099
+ }
9100
+ resolve(kind) {
9101
+ kind = assertStoreName("runtime kind", kind);
9102
+ const entry = this.list().find((candidate) => candidate.kind === kind);
9103
+ if (!entry) return void 0;
9104
+ if (hashFile(new URL(entry.moduleUrl)) !== entry.entryHash) throw new Error(`Registered runtime "${kind}" has changed; re-register it before starting a run.`);
9105
+ if (entry.lockfilePath && (!existsSync(entry.lockfilePath) || hashPath(entry.lockfilePath) !== entry.lockfileHash)) throw new Error(`Registered runtime "${kind}" lockfile has changed; re-register it before starting a run.`);
9106
+ return entry;
9107
+ }
9108
+ };
9109
+ function hashFile(url) {
9110
+ return createHash("sha256").update(readFileSync(url)).digest("hex");
9111
+ }
9112
+ function hashPath(path) {
9113
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
9114
+ }
9115
+ function isPackageSpecifier(specifier) {
9116
+ return !specifier.startsWith("file:") && !specifier.startsWith(".") && !specifier.startsWith("/");
9117
+ }
9118
+ function findLockfile(cwd) {
9119
+ let current = resolve(cwd);
9120
+ for (;;) {
9121
+ for (const name of LOCKFILE_NAMES) {
9122
+ const candidate = join(current, name);
9123
+ if (existsSync(candidate)) return candidate;
9124
+ }
9125
+ const parent = dirname(current);
9126
+ if (parent === current) return void 0;
9127
+ current = parent;
9128
+ }
9129
+ }
9130
+ function isRegistration(value) {
9131
+ if (!value || typeof value !== "object") return false;
9132
+ const entry = value;
9133
+ return typeof entry.kind === "string" && typeof entry.moduleUrl === "string" && entry.moduleUrl.startsWith("file:") && typeof entry.entryHash === "string" && typeof entry.registeredAt === "string" && (entry.lockfilePath === void 0 || typeof entry.lockfilePath === "string") && (entry.lockfileHash === void 0 || typeof entry.lockfileHash === "string") && entry.lockfilePath === void 0 === (entry.lockfileHash === void 0);
9134
+ }
9135
+ function writeRegistry(path, entries) {
9136
+ const temp = `${path}.${process.pid}.tmp`;
9137
+ writeFileSync(temp, `${JSON.stringify(entries, null, 2)}\n`, { mode: 384 });
9138
+ renameSync(temp, path);
9139
+ }
8900
9140
  var AgentServerModelDiscoveryError = class extends Error {
8901
9141
  name = "AgentServerModelDiscoveryError";
8902
9142
  constructor(code, message, statusCode, options) {
@@ -9194,6 +9434,16 @@ var AgentServerRouteSchemas = {
9194
9434
  ...problemResponse
9195
9435
  }
9196
9436
  },
9437
+ deleteProvider: {
9438
+ operationId: "deleteAgentServerProvider",
9439
+ tags: ["providers"],
9440
+ security: pairedSecurity,
9441
+ params: ProviderParamsSchema,
9442
+ response: {
9443
+ 204: Type.Any(),
9444
+ ...problemResponse
9445
+ }
9446
+ },
9197
9447
  listSubscriptions: {
9198
9448
  operationId: "listAgentServerSubscriptions",
9199
9449
  tags: ["subscriptions"],
@@ -9378,10 +9628,14 @@ function requestOperationSignal(request, shutdownSignal) {
9378
9628
  }
9379
9629
  function buildAgentServer(options) {
9380
9630
  const { pairing } = options;
9381
- const app = options.logger ? Fastify({
9631
+ const fastifyOptions = {
9382
9632
  bodyLimit: BODY_LIMIT,
9633
+ ...options.tls ? { https: options.tls } : {}
9634
+ };
9635
+ const app = options.logger ? Fastify({
9636
+ ...fastifyOptions,
9383
9637
  loggerInstance: options.logger
9384
- }) : Fastify({ bodyLimit: BODY_LIMIT });
9638
+ }) : Fastify(fastifyOptions);
9385
9639
  options.registerOpenApi?.(app);
9386
9640
  for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
9387
9641
  registerLoopbackSecurity(app, {
@@ -9440,12 +9694,12 @@ function buildAgentServer(options) {
9440
9694
  }));
9441
9695
  app.setErrorHandler(async (error, request, reply) => {
9442
9696
  const { statusCode, code, message } = normalizeAgentServerError(error);
9443
- if (statusCode === 500) request.log.error({
9697
+ if (statusCode === 500 || error instanceof AgentServerIdentityError) request.log[statusCode === 500 ? "error" : "warn"]({
9444
9698
  ...safeErrorContext(error),
9445
- code: "agent_server_request_failed",
9699
+ code: statusCode === 500 ? "agent_server_request_failed" : "agent_server_identity_rejected",
9446
9700
  method: request.method,
9447
9701
  route: request.routeOptions.url
9448
- }, "AgentServer request failed");
9702
+ }, statusCode === 500 ? "AgentServer request failed" : "AgentServer identity request rejected");
9449
9703
  return reply.code(statusCode).send({
9450
9704
  code,
9451
9705
  message
@@ -9672,12 +9926,29 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
9672
9926
  provider: FILE_SECRET_PROVIDER,
9673
9927
  key
9674
9928
  });
9675
- } else if (providers[providerId]?.apiKeyRef && providers[providerId].baseUrl === entry.baseUrl) entry.apiKeyRef = providers[providerId].apiKeyRef;
9929
+ } else if (providers[providerId]?.apiKeyRef) entry.apiKeyRef = providers[providerId].apiKeyRef;
9676
9930
  providers[providerId] = entry;
9677
9931
  store.writeProviders(providers);
9678
9932
  });
9679
9933
  return reply.code(200).send(providerView(entry));
9680
9934
  });
9935
+ app.delete("/v1/providers/:providerId", {
9936
+ schema: AgentServerRouteSchemas.deleteProvider,
9937
+ attachValidation: true
9938
+ }, async (request, reply) => {
9939
+ requirePairedOrigin(request);
9940
+ const { providerId: rawProviderId } = request.params;
9941
+ const providerId = assertProviderId(rawProviderId);
9942
+ await serialize(async () => {
9943
+ const providers = store.readProviders();
9944
+ const provider = providers[providerId];
9945
+ if (!provider) throw new AgentServerHttpError(404, "agent_server_provider_not_found", `Provider ${providerId} was not found`);
9946
+ if (provider.apiKeyRef) await options.secrets.delete(`pi-provider/${providerId}`);
9947
+ delete providers[providerId];
9948
+ store.writeProviders(providers);
9949
+ });
9950
+ return reply.code(204).send(null);
9951
+ });
9681
9952
  }
9682
9953
  function runViews(runs) {
9683
9954
  return runs.list(RUN_HISTORY_LIMIT).map((record) => ({
@@ -9829,12 +10100,23 @@ function safeErrorContext(error) {
9829
10100
  const applicationCode = safeErrorToken(error?.code);
9830
10101
  if (applicationCode) context["applicationCode"] = applicationCode;
9831
10102
  const cause = error instanceof Error ? error.cause : void 0;
10103
+ if (cause instanceof Error) {
10104
+ context["causeType"] = cause.name;
10105
+ const causeMessage = safeLogMessage(cause.message);
10106
+ if (causeMessage) context["causeMessage"] = causeMessage;
10107
+ }
9832
10108
  const fsCode = safeErrorToken(cause?.code);
9833
10109
  const syscall = safeErrorToken(cause?.syscall);
9834
10110
  if (fsCode) context["fsCode"] = fsCode;
9835
10111
  if (syscall) context["syscall"] = syscall;
10112
+ const causeStatus = cause?.statusCode;
10113
+ if (typeof causeStatus === "number") context["causeStatusCode"] = causeStatus;
9836
10114
  return context;
9837
10115
  }
10116
+ function safeLogMessage(value) {
10117
+ const normalized = value.replace(/[\r\n\t]/gu, " ").trim();
10118
+ return normalized ? normalized.slice(0, 500) : void 0;
10119
+ }
9838
10120
  function safeErrorToken(value) {
9839
10121
  return typeof value === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(value) ? value : void 0;
9840
10122
  }
@@ -9906,6 +10188,163 @@ function normalizeAgentServerError(error) {
9906
10188
  };
9907
10189
  }
9908
10190
  //#endregion
10191
+ //#region src/lib/agent-server/tls.ts
10192
+ var execFileAsync = promisify(execFile);
10193
+ var CA_COMMON_NAME = "MoltNet Local Agent CA";
10194
+ var LEAF_COMMON_NAME = "MoltNet Local Agent";
10195
+ var RENEW_BEFORE_MS = 720 * 60 * 60 * 1e3;
10196
+ function loginKeychainPath() {
10197
+ return join(homedir(), "Library", "Keychains", "login.keychain-db");
10198
+ }
10199
+ function pemPrivateKey(key) {
10200
+ return webcrypto.subtle.exportKey("pkcs8", key).then((der) => createPrivateKey({
10201
+ key: Buffer.from(der),
10202
+ format: "der",
10203
+ type: "pkcs8"
10204
+ }).export({
10205
+ format: "pem",
10206
+ type: "pkcs8"
10207
+ }).toString());
10208
+ }
10209
+ async function importCaKeyPair(pem) {
10210
+ const privateKey = createPrivateKey(pem);
10211
+ const privateDer = privateKey.export({
10212
+ format: "der",
10213
+ type: "pkcs8"
10214
+ });
10215
+ const publicDer = createPublicKey(privateKey).export({
10216
+ format: "der",
10217
+ type: "spki"
10218
+ });
10219
+ const [privateCryptoKey, publicCryptoKey] = await Promise.all([webcrypto.subtle.importKey("pkcs8", privateDer, {
10220
+ name: "ECDSA",
10221
+ namedCurve: "P-256"
10222
+ }, false, ["sign"]), webcrypto.subtle.importKey("spki", publicDer, {
10223
+ name: "ECDSA",
10224
+ namedCurve: "P-256"
10225
+ }, false, ["verify"])]);
10226
+ return {
10227
+ privateKey: privateCryptoKey,
10228
+ publicKey: publicCryptoKey
10229
+ };
10230
+ }
10231
+ async function localTlsMaterialFromDirectory(dir) {
10232
+ try {
10233
+ const [key, cert, ca] = await Promise.all([
10234
+ readFile(join(dir, "loopback-key.pem"), "utf8"),
10235
+ readFile(join(dir, "loopback-cert.pem"), "utf8"),
10236
+ readFile(join(dir, "local-ca.pem"), "utf8")
10237
+ ]);
10238
+ const parsed = new X509Certificate(cert);
10239
+ if (Date.parse(parsed.validTo) - Date.now() > RENEW_BEFORE_MS) return {
10240
+ key,
10241
+ cert,
10242
+ ca,
10243
+ fingerprint: new X509Certificate(ca).fingerprint256
10244
+ };
10245
+ } catch {}
10246
+ return null;
10247
+ }
10248
+ /** Creates a per-user CA and loopback-only leaf certificate under a 0700 directory. */
10249
+ async function ensureLocalTlsMaterial(root) {
10250
+ const dir = join(root, "tls");
10251
+ await mkdir(dir, {
10252
+ recursive: true,
10253
+ mode: 448
10254
+ });
10255
+ const existing = await localTlsMaterialFromDirectory(dir);
10256
+ if (existing) return existing;
10257
+ let ca;
10258
+ let caKeys;
10259
+ let caKey;
10260
+ try {
10261
+ ca = await readFile(join(dir, "local-ca.pem"), "utf8");
10262
+ caKeys = await importCaKeyPair(await readFile(join(dir, "local-ca-key.pem"), "utf8"));
10263
+ } catch {
10264
+ caKeys = await webcrypto.subtle.generateKey({
10265
+ name: "ECDSA",
10266
+ namedCurve: "P-256"
10267
+ }, true, ["sign", "verify"]);
10268
+ ca = (await X509CertificateGenerator.createSelfSigned({
10269
+ name: `CN=${CA_COMMON_NAME}`,
10270
+ keys: caKeys,
10271
+ notAfter: new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3),
10272
+ extensions: [new BasicConstraintsExtension(true, void 0, true), new KeyUsagesExtension(KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign, true)]
10273
+ })).toString("pem");
10274
+ caKey = await pemPrivateKey(caKeys.privateKey);
10275
+ }
10276
+ const leafKeys = await webcrypto.subtle.generateKey({
10277
+ name: "ECDSA",
10278
+ namedCurve: "P-256"
10279
+ }, true, ["sign", "verify"]);
10280
+ const leafCert = await X509CertificateGenerator.create({
10281
+ subject: `CN=${LEAF_COMMON_NAME}`,
10282
+ issuer: `CN=${CA_COMMON_NAME}`,
10283
+ publicKey: leafKeys.publicKey,
10284
+ signingKey: caKeys.privateKey,
10285
+ notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3),
10286
+ extensions: [
10287
+ new BasicConstraintsExtension(false, void 0, true),
10288
+ new KeyUsagesExtension(KeyUsageFlags.digitalSignature, true),
10289
+ new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth]),
10290
+ new SubjectAlternativeNameExtension([{
10291
+ type: IP,
10292
+ value: "127.0.0.1"
10293
+ }], true)
10294
+ ]
10295
+ });
10296
+ const key = await pemPrivateKey(leafKeys.privateKey);
10297
+ const cert = leafCert.toString("pem");
10298
+ const writes = [writeFile(join(dir, "loopback-key.pem"), key, { mode: 384 }), writeFile(join(dir, "loopback-cert.pem"), cert, { mode: 384 })];
10299
+ if (caKey) writes.push(writeFile(join(dir, "local-ca.pem"), ca, { mode: 384 }), writeFile(join(dir, "local-ca-key.pem"), caKey, { mode: 384 }));
10300
+ await Promise.all(writes);
10301
+ return {
10302
+ key,
10303
+ cert,
10304
+ ca,
10305
+ fingerprint: new X509Certificate(ca).fingerprint256
10306
+ };
10307
+ }
10308
+ async function trustLocalCa(root) {
10309
+ const caPath = join(root, "tls", "local-ca.pem");
10310
+ await execFileAsync("security", [
10311
+ "add-trusted-cert",
10312
+ "-d",
10313
+ "-r",
10314
+ "trustRoot",
10315
+ "-k",
10316
+ loginKeychainPath(),
10317
+ caPath
10318
+ ]);
10319
+ }
10320
+ async function isLocalCaTrusted(root) {
10321
+ const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
10322
+ try {
10323
+ const { stdout } = await execFileAsync("security", [
10324
+ "find-certificate",
10325
+ "-a",
10326
+ "-p",
10327
+ "-c",
10328
+ CA_COMMON_NAME,
10329
+ loginKeychainPath()
10330
+ ]);
10331
+ return stdout.includes(ca.trim());
10332
+ } catch {
10333
+ return false;
10334
+ }
10335
+ }
10336
+ async function removeLocalCa(root) {
10337
+ await execFileAsync("security", [
10338
+ "delete-certificate",
10339
+ "-Z",
10340
+ new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", ""),
10341
+ loginKeychainPath()
10342
+ ]);
10343
+ }
10344
+ function isMacos() {
10345
+ return process.platform === "darwin";
10346
+ }
10347
+ //#endregion
9909
10348
  //#region src/cli/server.ts
9910
10349
  /**
9911
10350
  * `moltnet-agent server` — per-user loopback supervisor (#2061).
@@ -9922,14 +10361,17 @@ async function runAgentServer(argv) {
9922
10361
  console.log(AGENT_SERVER_HELP);
9923
10362
  return 0;
9924
10363
  }
10364
+ const trustRequested = argv[0] === "trust";
10365
+ const commandArgs = trustRequested ? argv.slice(1) : argv;
9925
10366
  const envConfig = loadAgentServerEnvConfig();
9926
10367
  const { values } = parseArgs({
9927
- args: argv,
10368
+ args: commandArgs,
9928
10369
  options: {
9929
10370
  port: { type: "string" },
9930
10371
  "allowed-origins": { type: "string" },
9931
10372
  root: { type: "string" },
9932
- "api-url": { type: "string" }
10373
+ "api-url": { type: "string" },
10374
+ remove: { type: "boolean" }
9933
10375
  }
9934
10376
  });
9935
10377
  const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
@@ -9944,6 +10386,7 @@ async function runAgentServer(argv) {
9944
10386
  });
9945
10387
  const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
9946
10388
  const store = new AgentServerStore(root).ensure();
10389
+ if (trustRequested) return runTrustCommand(commandArgs, root);
9947
10390
  const { logger, shutdown: shutdownLogger } = createRootLogger({
9948
10391
  name: "agent-daemon.server",
9949
10392
  level: envConfig.logLevel || "info"
@@ -9968,8 +10411,10 @@ async function runAgentServer(argv) {
9968
10411
  secretProviders,
9969
10412
  externalSecretProviders,
9970
10413
  baseEnv: processEnvSnapshot(),
9971
- logger
10414
+ logger,
10415
+ runtimeRegistry: new RuntimeRegistry(store.root)
9972
10416
  });
10417
+ const tls = isMacos() ? await ensureTrustedLocalTls(root) : void 0;
9973
10418
  const app = buildAgentServer({
9974
10419
  store,
9975
10420
  secrets,
@@ -9979,7 +10424,11 @@ async function runAgentServer(argv) {
9979
10424
  runs,
9980
10425
  subscriptions,
9981
10426
  allowedOrigins,
9982
- selfOrigin: `http://127.0.0.1:${port}`,
10427
+ selfOrigin: `${tls ? "https" : "http"}://127.0.0.1:${port}`,
10428
+ ...tls ? { tls: {
10429
+ key: tls.key,
10430
+ cert: tls.cert
10431
+ } } : {},
9983
10432
  defaultApiUrl,
9984
10433
  version: "dev",
9985
10434
  logger,
@@ -10015,6 +10464,37 @@ async function runAgentServer(argv) {
10015
10464
  await shutdownLogger();
10016
10465
  }
10017
10466
  }
10467
+ async function runTrustCommand(argv, root) {
10468
+ if (!isMacos()) {
10469
+ console.error("Local HTTPS trust setup is currently supported on macOS only.");
10470
+ return 1;
10471
+ }
10472
+ if (argv.includes("--remove")) {
10473
+ await removeLocalCa(root);
10474
+ console.error("Removed the MoltNet local CA from your login keychain.");
10475
+ return 0;
10476
+ }
10477
+ await ensureTrustedLocalTls(root);
10478
+ console.error("MoltNet local HTTPS trust is ready for this macOS user.");
10479
+ return 0;
10480
+ }
10481
+ async function ensureTrustedLocalTls(root) {
10482
+ const material = await ensureLocalTlsMaterial(root);
10483
+ if (await isLocalCaTrusted(root)) return material;
10484
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Local HTTPS trust is not configured. Run `moltnet-agent server trust` from an interactive terminal.");
10485
+ const prompt = createInterface({
10486
+ input: process.stdin,
10487
+ output: process.stdout
10488
+ });
10489
+ try {
10490
+ const answer = await prompt.question(`Trust MoltNet's local CA (${material.fingerprint}) in this macOS login keychain? [y/N] `);
10491
+ if (!/^y(es)?$/i.test(answer.trim())) throw new Error("Local HTTPS trust was not approved.");
10492
+ } finally {
10493
+ prompt.close();
10494
+ }
10495
+ await trustLocalCa(root);
10496
+ return material;
10497
+ }
10018
10498
  function waitForAgentServerShutdown(runs, app, shutdownController) {
10019
10499
  return new Promise((resolvePromise) => {
10020
10500
  let shuttingDown = false;
@@ -10233,15 +10713,128 @@ function parseLimit(raw) {
10233
10713
  return value;
10234
10714
  }
10235
10715
  //#endregion
10716
+ //#region src/lib/update.ts
10717
+ var UPDATE_MANIFEST_URL = "https://themolt.net/download/manifest.json";
10718
+ var UPDATE_CACHE_TTL_MS = 1440 * 60 * 1e3;
10719
+ var UPDATE_ERROR_CACHE_TTL_MS = 300 * 1e3;
10720
+ function detectDaemonInstallMethod(executable = process.argv[1] ?? "") {
10721
+ const path = executable.replaceAll("\\", "/");
10722
+ if (path.includes("/node_modules/@themoltnet/agent-daemon/")) return "npm";
10723
+ if (path.includes("/.local/share/moltnet/") || path.includes("/opt/moltnet/")) return "bundle";
10724
+ return "direct";
10725
+ }
10726
+ function daemonUpdateCommand(method) {
10727
+ if (method === "npm") return "npm install -g @themoltnet/agent-daemon@latest";
10728
+ return "curl -fsSL https://themolt.net/install/agent | sh";
10729
+ }
10730
+ async function checkDaemonUpdate(input) {
10731
+ const installMethod = detectDaemonInstallMethod(input.executable ?? process.argv[1] ?? "moltnet-agent");
10732
+ const result = {
10733
+ currentVersion: normalizeVersion(input.currentVersion),
10734
+ updateAvailable: false,
10735
+ installMethod,
10736
+ releaseUrl: "https://themolt.net/download",
10737
+ command: daemonUpdateCommand(installMethod)
10738
+ };
10739
+ const now = input.now ?? /* @__PURE__ */ new Date();
10740
+ if (!input.force) {
10741
+ const cached = await readCache();
10742
+ const ttl = cached?.latest ? UPDATE_CACHE_TTL_MS : UPDATE_ERROR_CACHE_TTL_MS;
10743
+ if (cached && now.getTime() - Date.parse(cached.checkedAt) < ttl) {
10744
+ result.latestVersion = cached.latest;
10745
+ result.updateAvailable = compareVersions(cached.latest, result.currentVersion) > 0;
10746
+ return result;
10747
+ }
10748
+ }
10749
+ try {
10750
+ const response = await (input.fetchFn ?? fetch)(UPDATE_MANIFEST_URL, { signal: AbortSignal.timeout(5e3) });
10751
+ if (!response.ok) throw new Error(`manifest returned HTTP ${response.status}`);
10752
+ const latest = manifestVersion(await response.json());
10753
+ if (!latest) throw new Error("manifest has no valid agent version");
10754
+ await writeCache({
10755
+ checkedAt: now.toISOString(),
10756
+ latest
10757
+ }).catch(() => void 0);
10758
+ result.latestVersion = latest;
10759
+ result.updateAvailable = compareVersions(latest, result.currentVersion) > 0;
10760
+ return result;
10761
+ } catch (error) {
10762
+ await writeCache({
10763
+ checkedAt: now.toISOString(),
10764
+ error: error instanceof Error ? error.message : String(error)
10765
+ }).catch(() => void 0);
10766
+ throw new Error(`could not check for MoltNet agent updates: ${error instanceof Error ? error.message : String(error)}`);
10767
+ }
10768
+ }
10769
+ function manifestVersion(value) {
10770
+ if (!value || typeof value !== "object") return void 0;
10771
+ const agent = value.agent;
10772
+ if (!agent || typeof agent !== "object") return void 0;
10773
+ const version = agent.version;
10774
+ return typeof version === "string" && validVersion(version) ? normalizeVersion(version) : void 0;
10775
+ }
10776
+ function normalizeVersion(value) {
10777
+ return value.trim().replace(/^v/, "");
10778
+ }
10779
+ function validVersion(value) {
10780
+ return /^v?\d+\.\d+\.\d+$/.test(value);
10781
+ }
10782
+ function compareVersions(a, b) {
10783
+ if (!a || !validVersion(a) || !validVersion(b)) return 0;
10784
+ const left = normalizeVersion(a).split(".").map(Number);
10785
+ const right = normalizeVersion(b).split(".").map(Number);
10786
+ for (let i = 0; i < 3; i += 1) {
10787
+ const leftPart = left[i] ?? 0;
10788
+ const rightPart = right[i] ?? 0;
10789
+ if (leftPart > rightPart) return 1;
10790
+ if (leftPart < rightPart) return -1;
10791
+ }
10792
+ return 0;
10793
+ }
10794
+ function cachePath() {
10795
+ const env = loadUpdateEnvConfig();
10796
+ return join(env.xdgCacheHome || (platform() === "win32" ? env.localAppData || join(homedir(), "AppData", "Local") : join(homedir(), ".cache")), "moltnet", "updates", "agent.json");
10797
+ }
10798
+ async function readCache() {
10799
+ try {
10800
+ return JSON.parse(await readFile(cachePath(), "utf8"));
10801
+ } catch {
10802
+ return;
10803
+ }
10804
+ }
10805
+ async function writeCache(cache) {
10806
+ const path = cachePath();
10807
+ await mkdir(dirname(path), {
10808
+ recursive: true,
10809
+ mode: 448
10810
+ });
10811
+ await writeFile(path, JSON.stringify(cache), { mode: 384 });
10812
+ }
10813
+ //#endregion
10814
+ //#region src/version.ts
10815
+ var DAEMON_VERSION = "0.52.0";
10816
+ //#endregion
10236
10817
  //#region src/cli.ts
10237
10818
  async function runAgentDaemonCli(options) {
10238
10819
  const [subcommand, ...rest] = options.argv ?? process.argv.slice(2);
10820
+ if ([
10821
+ "poll",
10822
+ "drain",
10823
+ "serve"
10824
+ ].includes(subcommand ?? "") && !isWorkspaceInvocation()) checkDaemonUpdate({ currentVersion: DAEMON_VERSION }).then((update) => {
10825
+ if (update.updateAvailable) pino({ name: "moltnet.agent-daemon" }).info({
10826
+ event: "agent-daemon.update_available",
10827
+ update
10828
+ }, "A MoltNet agent update is available");
10829
+ }).catch(() => void 0);
10239
10830
  switch (subcommand) {
10240
10831
  case "poll": return runPoll(rest, options.runtime);
10241
10832
  case "once": return runOnce(rest, options.runtime);
10242
10833
  case "drain": return runDrain(rest, options.runtime);
10243
10834
  case "server": return runAgentServer(rest);
10244
10835
  case "sync-sessions": return runSyncSessions(rest);
10836
+ case "update": return runUpdate(rest);
10837
+ case "runtime": return runRuntime(rest);
10245
10838
  case "-h":
10246
10839
  case "--help":
10247
10840
  console.log(ROOT_USAGE);
@@ -10254,5 +10847,49 @@ async function runAgentDaemonCli(options) {
10254
10847
  return 1;
10255
10848
  }
10256
10849
  }
10850
+ function isWorkspaceInvocation() {
10851
+ return (process.argv[1] ?? "").replaceAll("\\", "/").includes("/apps/agent-daemon/src/");
10852
+ }
10853
+ async function runUpdate(argv) {
10854
+ if (argv[0] !== "check" || argv.slice(1).some((arg) => arg !== "--json")) {
10855
+ console.error("Usage: moltnet-agent update check [--json]");
10856
+ return 1;
10857
+ }
10858
+ try {
10859
+ const update = await checkDaemonUpdate({
10860
+ currentVersion: DAEMON_VERSION,
10861
+ force: true
10862
+ });
10863
+ if (argv.includes("--json")) console.log(JSON.stringify(update));
10864
+ else if (update.updateAvailable) console.log(`MoltNet agent ${update.latestVersion} is available (you have ${update.currentVersion}). Run: ${update.command}`);
10865
+ else console.log(`MoltNet agent ${update.currentVersion} is up to date.`);
10866
+ return 0;
10867
+ } catch (error) {
10868
+ console.error(error instanceof Error ? error.message : String(error));
10869
+ return 1;
10870
+ }
10871
+ }
10872
+ async function runRuntime(argv) {
10873
+ const config = loadAgentServerEnvConfig();
10874
+ const registry = new RuntimeRegistry(resolveAgentServerRoot({
10875
+ root: config.root,
10876
+ xdgConfigHome: config.xdgConfigHome
10877
+ }));
10878
+ const [command, ...rest] = argv;
10879
+ if (command === "list") {
10880
+ console.log(JSON.stringify(registry.list(), null, 2));
10881
+ return 0;
10882
+ }
10883
+ if (command === "unregister" && rest.length === 1) {
10884
+ if (!registry.unregister(rest[0])) throw new Error(`No runtime registered for "${rest[0]}".`);
10885
+ return 0;
10886
+ }
10887
+ if (command === "register" && rest.length === 2) {
10888
+ console.log(JSON.stringify(await registry.register(rest[0], rest[1]), null, 2));
10889
+ return 0;
10890
+ }
10891
+ console.error("Usage: moltnet-agent runtime <list|register <kind> <module>|unregister <kind>>");
10892
+ return 1;
10893
+ }
10257
10894
  //#endregion
10258
- export { runAgentDaemonCli };
10895
+ export { loadDaemonRuntimeAdapter as n, runAgentDaemonCli, extractRuntimeModule as t };
package/dist/main.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { defaultPiDaemonAdapter } from "./pi.js";
3
- import { runAgentDaemonCli } from "./cli.js";
4
- import { createRequire } from "node:module";
3
+ import { n as loadDaemonRuntimeAdapter, runAgentDaemonCli, t as extractRuntimeModule } from "./cli.js";
5
4
  import { registerInstrumentations } from "@opentelemetry/instrumentation";
6
5
  import { DnsInstrumentation } from "@opentelemetry/instrumentation-dns";
7
6
  import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
@@ -9,8 +8,6 @@ import { NetInstrumentation } from "@opentelemetry/instrumentation-net";
9
8
  import { PgInstrumentation } from "@opentelemetry/instrumentation-pg";
10
9
  import { PinoInstrumentation } from "@opentelemetry/instrumentation-pino";
11
10
  import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
12
- import { isAbsolute, resolve } from "node:path";
13
- import { pathToFileURL } from "node:url";
14
11
  //#region ../../libs/observability/src/instrumentation.ts
15
12
  /**
16
13
  * Register OTel auto-instrumentation for common Node.js modules.
@@ -101,70 +98,6 @@ function installSupervisorParentGuard(proc = process) {
101
98
  return true;
102
99
  }
103
100
  //#endregion
104
- //#region src/runtime-loader.ts
105
- function extractRuntimeModule(argv) {
106
- const remaining = [];
107
- let specifier;
108
- let parsingOptions = true;
109
- for (let index = 0; index < argv.length; index += 1) {
110
- const argument = argv[index];
111
- if (argument === "--") {
112
- parsingOptions = false;
113
- remaining.push(argument);
114
- continue;
115
- }
116
- if (!parsingOptions) {
117
- remaining.push(argument);
118
- continue;
119
- }
120
- let candidate;
121
- if (argument === "--runtime") {
122
- candidate = argv[index + 1];
123
- if (!candidate) throw new Error("Missing value for --runtime");
124
- index += 1;
125
- } else if (argument.startsWith("--runtime=")) {
126
- candidate = argument.slice(10);
127
- if (!candidate) throw new Error("Missing value for --runtime");
128
- } else {
129
- remaining.push(argument);
130
- continue;
131
- }
132
- if (specifier) throw new Error("--runtime may be specified only once");
133
- specifier = candidate;
134
- }
135
- return {
136
- argv: remaining,
137
- specifier
138
- };
139
- }
140
- function resolveRuntimeModuleUrl(specifier, cwd = process.cwd()) {
141
- if (specifier.startsWith("file:")) return new URL(specifier).href;
142
- if (isAbsolute(specifier) || specifier.startsWith("./") || specifier.startsWith("../")) return pathToFileURL(resolve(cwd, specifier)).href;
143
- return pathToFileURL(createRequire(resolve(cwd, "package.json")).resolve(specifier)).href;
144
- }
145
- async function loadDaemonRuntimeAdapter(specifier, options = {}) {
146
- let moduleUrl;
147
- try {
148
- moduleUrl = resolveRuntimeModuleUrl(specifier, options.cwd);
149
- } catch (cause) {
150
- throw new Error(`Unable to resolve runtime module "${specifier}" from ${options.cwd ?? process.cwd()}`, { cause });
151
- }
152
- let namespace;
153
- try {
154
- namespace = await import(moduleUrl);
155
- } catch (cause) {
156
- throw new Error(`Unable to load runtime module "${specifier}"`, { cause });
157
- }
158
- const adapter = namespace.default;
159
- if (!isDaemonRuntimeAdapter(adapter)) throw new Error(`Runtime module "${specifier}" must default-export a DaemonRuntimeAdapter`);
160
- return adapter;
161
- }
162
- function isDaemonRuntimeAdapter(value) {
163
- if (!value || typeof value !== "object") return false;
164
- const candidate = value;
165
- return typeof candidate.runtimeKind === "string" && candidate.runtimeKind.length > 0 && typeof candidate.prepare === "function";
166
- }
167
- //#endregion
168
101
  //#region src/main.ts
169
102
  installSupervisorParentGuard();
170
103
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.50.0",
3
+ "version": "0.52.0",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "description": "Universal MoltNet agent daemon host with a built-in Pi/Gondolin runtime and support for trusted operator-owned runtime modules. CLI: moltnet-agent.",
@@ -75,6 +75,7 @@
75
75
  "@opentelemetry/sdk-trace-base": "^2.5.1",
76
76
  "@opentelemetry/sdk-trace-node": "^2.5.1",
77
77
  "@opentelemetry/semantic-conventions": "^1.39.0",
78
+ "@peculiar/x509": "^2.0.0",
78
79
  "fastify": "^5.8.5",
79
80
  "fastify-plugin": "^5.1.0",
80
81
  "multiformats": "^13.3.0",
@@ -82,11 +83,12 @@
82
83
  "pino-opentelemetry-transport": "^3.0.0",
83
84
  "pino-pretty": "^13.1.3",
84
85
  "proper-lockfile": "4.1.2",
86
+ "reflect-metadata": "^0.2.2",
85
87
  "typebox": "^1.2.8",
88
+ "@themoltnet/pi-runtime": "0.14.1",
86
89
  "@themoltnet/agent-runtime": "0.45.2",
87
90
  "@themoltnet/os-keyring": "0.3.0",
88
- "@themoltnet/sdk": "0.140.0",
89
- "@themoltnet/pi-runtime": "0.14.1"
91
+ "@themoltnet/sdk": "0.140.0"
90
92
  },
91
93
  "devDependencies": {
92
94
  "@fastify/swagger": "^9.6.1",
@@ -96,15 +98,15 @@
96
98
  "vite": "^8.0.0",
97
99
  "vite-plugin-dts": "^4.5.4",
98
100
  "vitest": "^3.0.0",
101
+ "@moltnet/bootstrap": "0.1.0",
99
102
  "@moltnet/agent-eval": "0.1.0",
100
103
  "@moltnet/crypto-service": "0.1.0",
101
- "@moltnet/bootstrap": "0.1.0",
102
104
  "@moltnet/execution-integrations": "0.1.0",
103
- "@moltnet/loopback-companion": "0.1.0",
104
105
  "@moltnet/execution-plan": "0.1.0",
106
+ "@moltnet/loopback-companion": "0.1.0",
105
107
  "@moltnet/observability": "0.1.0",
106
- "@moltnet/models": "0.1.0",
107
108
  "@moltnet/runtime-profiles": "0.1.0",
109
+ "@moltnet/models": "0.1.0",
108
110
  "@moltnet/tasks": "0.1.0"
109
111
  },
110
112
  "nx": {