@themoltnet/agent-daemon 0.51.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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +272 -15
  3. package/package.json +9 -7
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.js CHANGED
@@ -16,7 +16,7 @@ import { FILE_SECRET_PROVIDER, FileSecretProvider, connect, createNodeSecretProv
16
16
  import { execFile, execFileSync, spawn } from "node:child_process";
17
17
  import { constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
18
18
  import { AuthenticationError, MoltNetError, agentKeyKey, assertTrustedConfigApiUrl, createExecutorAttestor, deriveMcpUrl, formatSecretReferenceString, identitySeedKey, parseSecretReferenceString, readConfig, register, requireSecureCredentialApiUrl, resolveAgentKey, resolveEnvSecretReference, resolveIdentitySeed, resolveOAuth2ClientSecret } from "@themoltnet/sdk";
19
- import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
19
+ import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual, webcrypto } from "node:crypto";
20
20
  import { once } from "node:events";
21
21
  import { metrics } from "@opentelemetry/api";
22
22
  import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
@@ -29,6 +29,7 @@ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic
29
29
  import { AsyncLocalStorage } from "node:async_hooks";
30
30
  import { mkdir, open, readFile, realpath, stat, writeFile } from "node:fs/promises";
31
31
  import { pipeline } from "node:stream/promises";
32
+ import { createInterface } from "node:readline/promises";
32
33
  import cors from "@fastify/cors";
33
34
  import helmet from "@fastify/helmet";
34
35
  import { lock, lockSync } from "proper-lockfile";
@@ -41,6 +42,8 @@ import { StringDecoder } from "node:string_decoder";
41
42
  import rateLimit from "@fastify/rate-limit";
42
43
  import Fastify from "fastify";
43
44
  import { isIP } from "node:net";
45
+ import "reflect-metadata";
46
+ import { BasicConstraintsExtension, ExtendedKeyUsage, ExtendedKeyUsageExtension, IP, KeyUsageFlags, KeyUsagesExtension, SubjectAlternativeNameExtension, X509CertificateGenerator } from "@peculiar/x509";
44
47
  import { createGzip } from "node:zlib";
45
48
  //#region ../../libs/tasks/src/rubric.ts
46
49
  /**
@@ -3431,6 +3434,8 @@ Commands:
3431
3434
  server Loopback supervisor for console-managed runs: pairing,
3432
3435
  agent/provider config store, and start/stop of poll/drain
3433
3436
  child processes. Binds 127.0.0.1 only.
3437
+ server trust
3438
+ Install the per-user macOS local-HTTPS CA after explicit consent.
3434
3439
  sync-sessions
3435
3440
  Repair durable runtime-session checkpoints from local slot files.
3436
3441
  update check
@@ -3582,6 +3587,10 @@ Options:
3582
3587
  (or MOLTNET_AGENT_SERVER_ROOT).
3583
3588
  --api-url <url> Default MoltNet API for new managed agents.
3584
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.
3585
3594
  `;
3586
3595
  //#endregion
3587
3596
  //#region src/lib/identity-pin.ts
@@ -3845,7 +3854,7 @@ async function resolveDaemonAgentIdentity(input) {
3845
3854
  }
3846
3855
  //#endregion
3847
3856
  //#region src/lib/correlation.ts
3848
- var execFileAsync = promisify(execFile);
3857
+ var execFileAsync$1 = promisify(execFile);
3849
3858
  var CORRELATION_TRAILER_KEY = "Moltnet-Correlation-Id";
3850
3859
  var CORRELATION_MARKER_RE = /<!--\s*moltnet-correlation:\s*([\w-]+)\s*-->/i;
3851
3860
  new RegExp(`^${CORRELATION_TRAILER_KEY}:\\s*(\\S+)\\s*$`, "m");
@@ -3892,7 +3901,7 @@ function makePrBodyAnchorWriter(deps) {
3892
3901
  function createGhCliClient() {
3893
3902
  return {
3894
3903
  async get({ owner, repo, number }) {
3895
- const { stdout } = await execFileAsync("gh", [
3904
+ const { stdout } = await execFileAsync$1("gh", [
3896
3905
  "api",
3897
3906
  `repos/${owner}/${repo}/pulls/${number}`,
3898
3907
  "--jq",
@@ -3901,7 +3910,7 @@ function createGhCliClient() {
3901
3910
  return JSON.parse(stdout);
3902
3911
  },
3903
3912
  async patch({ owner, repo, number }, body) {
3904
- await execFileAsync("gh", [
3913
+ await execFileAsync$1("gh", [
3905
3914
  "api",
3906
3915
  "-X",
3907
3916
  "PATCH",
@@ -7366,6 +7375,12 @@ function renderPairingApprovalPage(input) {
7366
7375
  </style>
7367
7376
  </head>
7368
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>
7369
7384
  <main>
7370
7385
  <h1>Allow this site to manage local MoltNet agents?</h1>
7371
7386
  <p><code>${escapeHtml(input.origin)}</code> asks to configure agents and start or stop local daemon runs on this machine.</p>
@@ -8148,7 +8163,7 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
8148
8163
  } catch (cause) {
8149
8164
  if (!registeredIdentityId && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
8150
8165
  store.clearPendingRegistration(alias);
8151
- throw new AgentServerIdentityError("registration_failed", `registration for "${alias}" was rejected`, { cause });
8166
+ throw new AgentServerIdentityError("registration_failed", registrationRejectionMessage(alias, cause), { cause });
8152
8167
  }
8153
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 });
8154
8169
  throw cause;
@@ -8156,6 +8171,9 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
8156
8171
  releaseAlias();
8157
8172
  }
8158
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
+ }
8159
8177
  /** Resume a fully persisted registration or explicitly abandon local recovery. */
8160
8178
  async function reconcileManagedRegistration(store, secrets, aliasInput, action, connectAgent = connect, signal) {
8161
8179
  const alias = assertStoreName("agent name", aliasInput);
@@ -9416,6 +9434,16 @@ var AgentServerRouteSchemas = {
9416
9434
  ...problemResponse
9417
9435
  }
9418
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
+ },
9419
9447
  listSubscriptions: {
9420
9448
  operationId: "listAgentServerSubscriptions",
9421
9449
  tags: ["subscriptions"],
@@ -9600,10 +9628,14 @@ function requestOperationSignal(request, shutdownSignal) {
9600
9628
  }
9601
9629
  function buildAgentServer(options) {
9602
9630
  const { pairing } = options;
9603
- const app = options.logger ? Fastify({
9631
+ const fastifyOptions = {
9604
9632
  bodyLimit: BODY_LIMIT,
9633
+ ...options.tls ? { https: options.tls } : {}
9634
+ };
9635
+ const app = options.logger ? Fastify({
9636
+ ...fastifyOptions,
9605
9637
  loggerInstance: options.logger
9606
- }) : Fastify({ bodyLimit: BODY_LIMIT });
9638
+ }) : Fastify(fastifyOptions);
9607
9639
  options.registerOpenApi?.(app);
9608
9640
  for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
9609
9641
  registerLoopbackSecurity(app, {
@@ -9662,12 +9694,12 @@ function buildAgentServer(options) {
9662
9694
  }));
9663
9695
  app.setErrorHandler(async (error, request, reply) => {
9664
9696
  const { statusCode, code, message } = normalizeAgentServerError(error);
9665
- if (statusCode === 500) request.log.error({
9697
+ if (statusCode === 500 || error instanceof AgentServerIdentityError) request.log[statusCode === 500 ? "error" : "warn"]({
9666
9698
  ...safeErrorContext(error),
9667
- code: "agent_server_request_failed",
9699
+ code: statusCode === 500 ? "agent_server_request_failed" : "agent_server_identity_rejected",
9668
9700
  method: request.method,
9669
9701
  route: request.routeOptions.url
9670
- }, "AgentServer request failed");
9702
+ }, statusCode === 500 ? "AgentServer request failed" : "AgentServer identity request rejected");
9671
9703
  return reply.code(statusCode).send({
9672
9704
  code,
9673
9705
  message
@@ -9894,12 +9926,29 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
9894
9926
  provider: FILE_SECRET_PROVIDER,
9895
9927
  key
9896
9928
  });
9897
- } 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;
9898
9930
  providers[providerId] = entry;
9899
9931
  store.writeProviders(providers);
9900
9932
  });
9901
9933
  return reply.code(200).send(providerView(entry));
9902
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
+ });
9903
9952
  }
9904
9953
  function runViews(runs) {
9905
9954
  return runs.list(RUN_HISTORY_LIMIT).map((record) => ({
@@ -10051,12 +10100,23 @@ function safeErrorContext(error) {
10051
10100
  const applicationCode = safeErrorToken(error?.code);
10052
10101
  if (applicationCode) context["applicationCode"] = applicationCode;
10053
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
+ }
10054
10108
  const fsCode = safeErrorToken(cause?.code);
10055
10109
  const syscall = safeErrorToken(cause?.syscall);
10056
10110
  if (fsCode) context["fsCode"] = fsCode;
10057
10111
  if (syscall) context["syscall"] = syscall;
10112
+ const causeStatus = cause?.statusCode;
10113
+ if (typeof causeStatus === "number") context["causeStatusCode"] = causeStatus;
10058
10114
  return context;
10059
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
+ }
10060
10120
  function safeErrorToken(value) {
10061
10121
  return typeof value === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(value) ? value : void 0;
10062
10122
  }
@@ -10128,6 +10188,163 @@ function normalizeAgentServerError(error) {
10128
10188
  };
10129
10189
  }
10130
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
10131
10348
  //#region src/cli/server.ts
10132
10349
  /**
10133
10350
  * `moltnet-agent server` — per-user loopback supervisor (#2061).
@@ -10144,14 +10361,17 @@ async function runAgentServer(argv) {
10144
10361
  console.log(AGENT_SERVER_HELP);
10145
10362
  return 0;
10146
10363
  }
10364
+ const trustRequested = argv[0] === "trust";
10365
+ const commandArgs = trustRequested ? argv.slice(1) : argv;
10147
10366
  const envConfig = loadAgentServerEnvConfig();
10148
10367
  const { values } = parseArgs({
10149
- args: argv,
10368
+ args: commandArgs,
10150
10369
  options: {
10151
10370
  port: { type: "string" },
10152
10371
  "allowed-origins": { type: "string" },
10153
10372
  root: { type: "string" },
10154
- "api-url": { type: "string" }
10373
+ "api-url": { type: "string" },
10374
+ remove: { type: "boolean" }
10155
10375
  }
10156
10376
  });
10157
10377
  const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
@@ -10166,6 +10386,7 @@ async function runAgentServer(argv) {
10166
10386
  });
10167
10387
  const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
10168
10388
  const store = new AgentServerStore(root).ensure();
10389
+ if (trustRequested) return runTrustCommand(commandArgs, root);
10169
10390
  const { logger, shutdown: shutdownLogger } = createRootLogger({
10170
10391
  name: "agent-daemon.server",
10171
10392
  level: envConfig.logLevel || "info"
@@ -10193,6 +10414,7 @@ async function runAgentServer(argv) {
10193
10414
  logger,
10194
10415
  runtimeRegistry: new RuntimeRegistry(store.root)
10195
10416
  });
10417
+ const tls = isMacos() ? await ensureTrustedLocalTls(root) : void 0;
10196
10418
  const app = buildAgentServer({
10197
10419
  store,
10198
10420
  secrets,
@@ -10202,7 +10424,11 @@ async function runAgentServer(argv) {
10202
10424
  runs,
10203
10425
  subscriptions,
10204
10426
  allowedOrigins,
10205
- 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
+ } } : {},
10206
10432
  defaultApiUrl,
10207
10433
  version: "dev",
10208
10434
  logger,
@@ -10238,6 +10464,37 @@ async function runAgentServer(argv) {
10238
10464
  await shutdownLogger();
10239
10465
  }
10240
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
+ }
10241
10498
  function waitForAgentServerShutdown(runs, app, shutdownController) {
10242
10499
  return new Promise((resolvePromise) => {
10243
10500
  let shuttingDown = false;
@@ -10555,7 +10812,7 @@ async function writeCache(cache) {
10555
10812
  }
10556
10813
  //#endregion
10557
10814
  //#region src/version.ts
10558
- var DAEMON_VERSION = "0.51.0";
10815
+ var DAEMON_VERSION = "0.52.0";
10559
10816
  //#endregion
10560
10817
  //#region src/cli.ts
10561
10818
  async function runAgentDaemonCli(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.51.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,9 +83,10 @@
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",
86
- "@themoltnet/agent-runtime": "0.45.2",
87
88
  "@themoltnet/pi-runtime": "0.14.1",
89
+ "@themoltnet/agent-runtime": "0.45.2",
88
90
  "@themoltnet/os-keyring": "0.3.0",
89
91
  "@themoltnet/sdk": "0.140.0"
90
92
  },
@@ -96,16 +98,16 @@
96
98
  "vite": "^8.0.0",
97
99
  "vite-plugin-dts": "^4.5.4",
98
100
  "vitest": "^3.0.0",
99
- "@moltnet/agent-eval": "0.1.0",
100
101
  "@moltnet/bootstrap": "0.1.0",
102
+ "@moltnet/agent-eval": "0.1.0",
101
103
  "@moltnet/crypto-service": "0.1.0",
102
- "@moltnet/execution-plan": "0.1.0",
103
104
  "@moltnet/execution-integrations": "0.1.0",
104
- "@moltnet/models": "0.1.0",
105
+ "@moltnet/execution-plan": "0.1.0",
105
106
  "@moltnet/loopback-companion": "0.1.0",
106
- "@moltnet/tasks": "0.1.0",
107
107
  "@moltnet/observability": "0.1.0",
108
- "@moltnet/runtime-profiles": "0.1.0"
108
+ "@moltnet/runtime-profiles": "0.1.0",
109
+ "@moltnet/models": "0.1.0",
110
+ "@moltnet/tasks": "0.1.0"
109
111
  },
110
112
  "nx": {
111
113
  "projectType": "application",