@themoltnet/agent-daemon 0.51.0 → 0.53.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 +16 -8
  2. package/dist/cli.js +279 -15
  3. package/package.json +11 -9
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
@@ -135,14 +135,22 @@ of `MOLTNET_PRIVATE_KEY`.
135
135
  An agent key used by the daemon needs this least-privilege scope set:
136
136
 
137
137
  ```text
138
- agent:profile runtime:read task:read task:claim task:execute
138
+ agent:profile crypto:sign runtime:read task:read task:claim task:execute
139
139
  ```
140
140
 
141
- The Console selects these five scopes by default. Knowledge-enabled workers
142
- must add `diary:read`, `diary:write`, `pack:read`, and `pack:write` when the key
143
- is issued. Runtime policy can narrow key authority but cannot add missing
144
- scopes, and existing keys are never widened automatically; issue a replacement
145
- credential when broader authority is required.
141
+ The Console selects these scopes by default. Knowledge-enabled workers must add
142
+ `diary:read`, `diary:write`, `pack:read`, and `pack:write` when the key is
143
+ issued.
144
+
145
+ `crypto:sign` is in the minimum because host-capability signing runs on the
146
+ daemon's own credential: the local seed signer calls the signing-request
147
+ endpoints, which require it. Without it the daemon refuses to start rather than
148
+ booting cleanly and failing the first time guest code signs a diary entry or a
149
+ commit. Keys issued with the older five-scope set must be reissued.
150
+
151
+ Runtime policy can narrow key authority but cannot add missing scopes, and
152
+ existing keys are never widened automatically; issue a replacement credential
153
+ when broader authority is required.
146
154
 
147
155
  ### Pi provider auth
148
156
 
@@ -196,7 +204,7 @@ host-exec command is safe to run without a dialog.
196
204
  ### Remote runtime profiles
197
205
 
198
206
  The canonical user-facing guide lives in the public docs:
199
- [Running Agents § Runtime Profiles](https://docs.themolt.net/operate/running-agents#runtime-profiles).
207
+ [Running Agents § Runtime Profiles](https://docs.themolt.net/operate/runtime-profiles).
200
208
 
201
209
  ## Correlation anchors
202
210
 
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
  /**
@@ -776,9 +779,16 @@ var ALL_CREDENTIAL_SCOPES = Object.freeze(Object.values(CREDENTIAL_SCOPES));
776
779
  /**
777
780
  * Minimum grant for the agent daemon. Task credentials attenuate this further
778
781
  * to `task:execute` alone.
782
+ *
783
+ * `crypto:sign` is part of the minimum because host-capability signing runs on
784
+ * the daemon's own credential: the local seed signer calls the signing-request
785
+ * endpoints, which require it. A grant without it produces a daemon that boots
786
+ * cleanly and then fails the first time guest code signs a diary entry or a
787
+ * commit.
779
788
  */
780
789
  var AGENT_CREDENTIAL_SCOPES = [
781
790
  CREDENTIAL_SCOPES.AgentProfile,
791
+ CREDENTIAL_SCOPES.CryptoSign,
782
792
  CREDENTIAL_SCOPES.RuntimeRead,
783
793
  CREDENTIAL_SCOPES.TaskRead,
784
794
  CREDENTIAL_SCOPES.TaskClaim,
@@ -3431,6 +3441,8 @@ Commands:
3431
3441
  server Loopback supervisor for console-managed runs: pairing,
3432
3442
  agent/provider config store, and start/stop of poll/drain
3433
3443
  child processes. Binds 127.0.0.1 only.
3444
+ server trust
3445
+ Install the per-user macOS local-HTTPS CA after explicit consent.
3434
3446
  sync-sessions
3435
3447
  Repair durable runtime-session checkpoints from local slot files.
3436
3448
  update check
@@ -3582,6 +3594,10 @@ Options:
3582
3594
  (or MOLTNET_AGENT_SERVER_ROOT).
3583
3595
  --api-url <url> Default MoltNet API for new managed agents.
3584
3596
  Default: https://api.themolt.net.
3597
+
3598
+ On macOS, the first interactive run asks to trust a per-user local CA in the
3599
+ login keychain and serves HTTPS. Run \`agent-daemon server trust --remove\` to
3600
+ remove that exact CA. Linux continues to use the Chromium PNA HTTP path.
3585
3601
  `;
3586
3602
  //#endregion
3587
3603
  //#region src/lib/identity-pin.ts
@@ -3845,7 +3861,7 @@ async function resolveDaemonAgentIdentity(input) {
3845
3861
  }
3846
3862
  //#endregion
3847
3863
  //#region src/lib/correlation.ts
3848
- var execFileAsync = promisify(execFile);
3864
+ var execFileAsync$1 = promisify(execFile);
3849
3865
  var CORRELATION_TRAILER_KEY = "Moltnet-Correlation-Id";
3850
3866
  var CORRELATION_MARKER_RE = /<!--\s*moltnet-correlation:\s*([\w-]+)\s*-->/i;
3851
3867
  new RegExp(`^${CORRELATION_TRAILER_KEY}:\\s*(\\S+)\\s*$`, "m");
@@ -3892,7 +3908,7 @@ function makePrBodyAnchorWriter(deps) {
3892
3908
  function createGhCliClient() {
3893
3909
  return {
3894
3910
  async get({ owner, repo, number }) {
3895
- const { stdout } = await execFileAsync("gh", [
3911
+ const { stdout } = await execFileAsync$1("gh", [
3896
3912
  "api",
3897
3913
  `repos/${owner}/${repo}/pulls/${number}`,
3898
3914
  "--jq",
@@ -3901,7 +3917,7 @@ function createGhCliClient() {
3901
3917
  return JSON.parse(stdout);
3902
3918
  },
3903
3919
  async patch({ owner, repo, number }, body) {
3904
- await execFileAsync("gh", [
3920
+ await execFileAsync$1("gh", [
3905
3921
  "api",
3906
3922
  "-X",
3907
3923
  "PATCH",
@@ -7366,6 +7382,12 @@ function renderPairingApprovalPage(input) {
7366
7382
  </style>
7367
7383
  </head>
7368
7384
  <body>
7385
+ <script>
7386
+ // The Console must remain this popup's opener until it finishes navigating
7387
+ // from about:blank. Safari rejects that cross-origin navigation otherwise.
7388
+ // Once this trusted local approval document has loaded, it needs no opener.
7389
+ window.opener = null;
7390
+ <\/script>
7369
7391
  <main>
7370
7392
  <h1>Allow this site to manage local MoltNet agents?</h1>
7371
7393
  <p><code>${escapeHtml(input.origin)}</code> asks to configure agents and start or stop local daemon runs on this machine.</p>
@@ -8148,7 +8170,7 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
8148
8170
  } catch (cause) {
8149
8171
  if (!registeredIdentityId && cause instanceof MoltNetError && cause.statusCode !== void 0 && cause.statusCode >= 400 && cause.statusCode < 500) {
8150
8172
  store.clearPendingRegistration(alias);
8151
- throw new AgentServerIdentityError("registration_failed", `registration for "${alias}" was rejected`, { cause });
8173
+ throw new AgentServerIdentityError("registration_failed", registrationRejectionMessage(alias, cause), { cause });
8152
8174
  }
8153
8175
  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
8176
  throw cause;
@@ -8156,6 +8178,9 @@ async function createManagedAgent(store, secrets, input, connectAgent = connect)
8156
8178
  releaseAlias();
8157
8179
  }
8158
8180
  }
8181
+ function registrationRejectionMessage(alias, cause) {
8182
+ return `registration for "${alias}" was rejected${cause.statusCode === void 0 ? "" : ` (${cause.statusCode})`}: ${cause.detail?.trim() || cause.message}`;
8183
+ }
8159
8184
  /** Resume a fully persisted registration or explicitly abandon local recovery. */
8160
8185
  async function reconcileManagedRegistration(store, secrets, aliasInput, action, connectAgent = connect, signal) {
8161
8186
  const alias = assertStoreName("agent name", aliasInput);
@@ -9416,6 +9441,16 @@ var AgentServerRouteSchemas = {
9416
9441
  ...problemResponse
9417
9442
  }
9418
9443
  },
9444
+ deleteProvider: {
9445
+ operationId: "deleteAgentServerProvider",
9446
+ tags: ["providers"],
9447
+ security: pairedSecurity,
9448
+ params: ProviderParamsSchema,
9449
+ response: {
9450
+ 204: Type.Any(),
9451
+ ...problemResponse
9452
+ }
9453
+ },
9419
9454
  listSubscriptions: {
9420
9455
  operationId: "listAgentServerSubscriptions",
9421
9456
  tags: ["subscriptions"],
@@ -9600,10 +9635,14 @@ function requestOperationSignal(request, shutdownSignal) {
9600
9635
  }
9601
9636
  function buildAgentServer(options) {
9602
9637
  const { pairing } = options;
9603
- const app = options.logger ? Fastify({
9638
+ const fastifyOptions = {
9604
9639
  bodyLimit: BODY_LIMIT,
9640
+ ...options.tls ? { https: options.tls } : {}
9641
+ };
9642
+ const app = options.logger ? Fastify({
9643
+ ...fastifyOptions,
9605
9644
  loggerInstance: options.logger
9606
- }) : Fastify({ bodyLimit: BODY_LIMIT });
9645
+ }) : Fastify(fastifyOptions);
9607
9646
  options.registerOpenApi?.(app);
9608
9647
  for (const schema of AGENT_SERVER_SCHEMAS) app.addSchema(schema);
9609
9648
  registerLoopbackSecurity(app, {
@@ -9662,12 +9701,12 @@ function buildAgentServer(options) {
9662
9701
  }));
9663
9702
  app.setErrorHandler(async (error, request, reply) => {
9664
9703
  const { statusCode, code, message } = normalizeAgentServerError(error);
9665
- if (statusCode === 500) request.log.error({
9704
+ if (statusCode === 500 || error instanceof AgentServerIdentityError) request.log[statusCode === 500 ? "error" : "warn"]({
9666
9705
  ...safeErrorContext(error),
9667
- code: "agent_server_request_failed",
9706
+ code: statusCode === 500 ? "agent_server_request_failed" : "agent_server_identity_rejected",
9668
9707
  method: request.method,
9669
9708
  route: request.routeOptions.url
9670
- }, "AgentServer request failed");
9709
+ }, statusCode === 500 ? "AgentServer request failed" : "AgentServer identity request rejected");
9671
9710
  return reply.code(statusCode).send({
9672
9711
  code,
9673
9712
  message
@@ -9894,12 +9933,29 @@ function registerProviderRoutes(app, options, requirePairedOrigin) {
9894
9933
  provider: FILE_SECRET_PROVIDER,
9895
9934
  key
9896
9935
  });
9897
- } else if (providers[providerId]?.apiKeyRef && providers[providerId].baseUrl === entry.baseUrl) entry.apiKeyRef = providers[providerId].apiKeyRef;
9936
+ } else if (providers[providerId]?.apiKeyRef) entry.apiKeyRef = providers[providerId].apiKeyRef;
9898
9937
  providers[providerId] = entry;
9899
9938
  store.writeProviders(providers);
9900
9939
  });
9901
9940
  return reply.code(200).send(providerView(entry));
9902
9941
  });
9942
+ app.delete("/v1/providers/:providerId", {
9943
+ schema: AgentServerRouteSchemas.deleteProvider,
9944
+ attachValidation: true
9945
+ }, async (request, reply) => {
9946
+ requirePairedOrigin(request);
9947
+ const { providerId: rawProviderId } = request.params;
9948
+ const providerId = assertProviderId(rawProviderId);
9949
+ await serialize(async () => {
9950
+ const providers = store.readProviders();
9951
+ const provider = providers[providerId];
9952
+ if (!provider) throw new AgentServerHttpError(404, "agent_server_provider_not_found", `Provider ${providerId} was not found`);
9953
+ if (provider.apiKeyRef) await options.secrets.delete(`pi-provider/${providerId}`);
9954
+ delete providers[providerId];
9955
+ store.writeProviders(providers);
9956
+ });
9957
+ return reply.code(204).send(null);
9958
+ });
9903
9959
  }
9904
9960
  function runViews(runs) {
9905
9961
  return runs.list(RUN_HISTORY_LIMIT).map((record) => ({
@@ -10051,12 +10107,23 @@ function safeErrorContext(error) {
10051
10107
  const applicationCode = safeErrorToken(error?.code);
10052
10108
  if (applicationCode) context["applicationCode"] = applicationCode;
10053
10109
  const cause = error instanceof Error ? error.cause : void 0;
10110
+ if (cause instanceof Error) {
10111
+ context["causeType"] = cause.name;
10112
+ const causeMessage = safeLogMessage(cause.message);
10113
+ if (causeMessage) context["causeMessage"] = causeMessage;
10114
+ }
10054
10115
  const fsCode = safeErrorToken(cause?.code);
10055
10116
  const syscall = safeErrorToken(cause?.syscall);
10056
10117
  if (fsCode) context["fsCode"] = fsCode;
10057
10118
  if (syscall) context["syscall"] = syscall;
10119
+ const causeStatus = cause?.statusCode;
10120
+ if (typeof causeStatus === "number") context["causeStatusCode"] = causeStatus;
10058
10121
  return context;
10059
10122
  }
10123
+ function safeLogMessage(value) {
10124
+ const normalized = value.replace(/[\r\n\t]/gu, " ").trim();
10125
+ return normalized ? normalized.slice(0, 500) : void 0;
10126
+ }
10060
10127
  function safeErrorToken(value) {
10061
10128
  return typeof value === "string" && /^[a-z0-9_:-]{1,64}$/iu.test(value) ? value : void 0;
10062
10129
  }
@@ -10128,6 +10195,163 @@ function normalizeAgentServerError(error) {
10128
10195
  };
10129
10196
  }
10130
10197
  //#endregion
10198
+ //#region src/lib/agent-server/tls.ts
10199
+ var execFileAsync = promisify(execFile);
10200
+ var CA_COMMON_NAME = "MoltNet Local Agent CA";
10201
+ var LEAF_COMMON_NAME = "MoltNet Local Agent";
10202
+ var RENEW_BEFORE_MS = 720 * 60 * 60 * 1e3;
10203
+ function loginKeychainPath() {
10204
+ return join(homedir(), "Library", "Keychains", "login.keychain-db");
10205
+ }
10206
+ function pemPrivateKey(key) {
10207
+ return webcrypto.subtle.exportKey("pkcs8", key).then((der) => createPrivateKey({
10208
+ key: Buffer.from(der),
10209
+ format: "der",
10210
+ type: "pkcs8"
10211
+ }).export({
10212
+ format: "pem",
10213
+ type: "pkcs8"
10214
+ }).toString());
10215
+ }
10216
+ async function importCaKeyPair(pem) {
10217
+ const privateKey = createPrivateKey(pem);
10218
+ const privateDer = privateKey.export({
10219
+ format: "der",
10220
+ type: "pkcs8"
10221
+ });
10222
+ const publicDer = createPublicKey(privateKey).export({
10223
+ format: "der",
10224
+ type: "spki"
10225
+ });
10226
+ const [privateCryptoKey, publicCryptoKey] = await Promise.all([webcrypto.subtle.importKey("pkcs8", privateDer, {
10227
+ name: "ECDSA",
10228
+ namedCurve: "P-256"
10229
+ }, false, ["sign"]), webcrypto.subtle.importKey("spki", publicDer, {
10230
+ name: "ECDSA",
10231
+ namedCurve: "P-256"
10232
+ }, false, ["verify"])]);
10233
+ return {
10234
+ privateKey: privateCryptoKey,
10235
+ publicKey: publicCryptoKey
10236
+ };
10237
+ }
10238
+ async function localTlsMaterialFromDirectory(dir) {
10239
+ try {
10240
+ const [key, cert, ca] = await Promise.all([
10241
+ readFile(join(dir, "loopback-key.pem"), "utf8"),
10242
+ readFile(join(dir, "loopback-cert.pem"), "utf8"),
10243
+ readFile(join(dir, "local-ca.pem"), "utf8")
10244
+ ]);
10245
+ const parsed = new X509Certificate(cert);
10246
+ if (Date.parse(parsed.validTo) - Date.now() > RENEW_BEFORE_MS) return {
10247
+ key,
10248
+ cert,
10249
+ ca,
10250
+ fingerprint: new X509Certificate(ca).fingerprint256
10251
+ };
10252
+ } catch {}
10253
+ return null;
10254
+ }
10255
+ /** Creates a per-user CA and loopback-only leaf certificate under a 0700 directory. */
10256
+ async function ensureLocalTlsMaterial(root) {
10257
+ const dir = join(root, "tls");
10258
+ await mkdir(dir, {
10259
+ recursive: true,
10260
+ mode: 448
10261
+ });
10262
+ const existing = await localTlsMaterialFromDirectory(dir);
10263
+ if (existing) return existing;
10264
+ let ca;
10265
+ let caKeys;
10266
+ let caKey;
10267
+ try {
10268
+ ca = await readFile(join(dir, "local-ca.pem"), "utf8");
10269
+ caKeys = await importCaKeyPair(await readFile(join(dir, "local-ca-key.pem"), "utf8"));
10270
+ } catch {
10271
+ caKeys = await webcrypto.subtle.generateKey({
10272
+ name: "ECDSA",
10273
+ namedCurve: "P-256"
10274
+ }, true, ["sign", "verify"]);
10275
+ ca = (await X509CertificateGenerator.createSelfSigned({
10276
+ name: `CN=${CA_COMMON_NAME}`,
10277
+ keys: caKeys,
10278
+ notAfter: new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3),
10279
+ extensions: [new BasicConstraintsExtension(true, void 0, true), new KeyUsagesExtension(KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign, true)]
10280
+ })).toString("pem");
10281
+ caKey = await pemPrivateKey(caKeys.privateKey);
10282
+ }
10283
+ const leafKeys = await webcrypto.subtle.generateKey({
10284
+ name: "ECDSA",
10285
+ namedCurve: "P-256"
10286
+ }, true, ["sign", "verify"]);
10287
+ const leafCert = await X509CertificateGenerator.create({
10288
+ subject: `CN=${LEAF_COMMON_NAME}`,
10289
+ issuer: `CN=${CA_COMMON_NAME}`,
10290
+ publicKey: leafKeys.publicKey,
10291
+ signingKey: caKeys.privateKey,
10292
+ notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3),
10293
+ extensions: [
10294
+ new BasicConstraintsExtension(false, void 0, true),
10295
+ new KeyUsagesExtension(KeyUsageFlags.digitalSignature, true),
10296
+ new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth]),
10297
+ new SubjectAlternativeNameExtension([{
10298
+ type: IP,
10299
+ value: "127.0.0.1"
10300
+ }], true)
10301
+ ]
10302
+ });
10303
+ const key = await pemPrivateKey(leafKeys.privateKey);
10304
+ const cert = leafCert.toString("pem");
10305
+ const writes = [writeFile(join(dir, "loopback-key.pem"), key, { mode: 384 }), writeFile(join(dir, "loopback-cert.pem"), cert, { mode: 384 })];
10306
+ if (caKey) writes.push(writeFile(join(dir, "local-ca.pem"), ca, { mode: 384 }), writeFile(join(dir, "local-ca-key.pem"), caKey, { mode: 384 }));
10307
+ await Promise.all(writes);
10308
+ return {
10309
+ key,
10310
+ cert,
10311
+ ca,
10312
+ fingerprint: new X509Certificate(ca).fingerprint256
10313
+ };
10314
+ }
10315
+ async function trustLocalCa(root) {
10316
+ const caPath = join(root, "tls", "local-ca.pem");
10317
+ await execFileAsync("security", [
10318
+ "add-trusted-cert",
10319
+ "-d",
10320
+ "-r",
10321
+ "trustRoot",
10322
+ "-k",
10323
+ loginKeychainPath(),
10324
+ caPath
10325
+ ]);
10326
+ }
10327
+ async function isLocalCaTrusted(root) {
10328
+ const ca = await readFile(join(root, "tls", "local-ca.pem"), "utf8");
10329
+ try {
10330
+ const { stdout } = await execFileAsync("security", [
10331
+ "find-certificate",
10332
+ "-a",
10333
+ "-p",
10334
+ "-c",
10335
+ CA_COMMON_NAME,
10336
+ loginKeychainPath()
10337
+ ]);
10338
+ return stdout.includes(ca.trim());
10339
+ } catch {
10340
+ return false;
10341
+ }
10342
+ }
10343
+ async function removeLocalCa(root) {
10344
+ await execFileAsync("security", [
10345
+ "delete-certificate",
10346
+ "-Z",
10347
+ new X509Certificate(await readFile(join(root, "tls", "local-ca.pem"), "utf8")).fingerprint256.replaceAll(":", ""),
10348
+ loginKeychainPath()
10349
+ ]);
10350
+ }
10351
+ function isMacos() {
10352
+ return process.platform === "darwin";
10353
+ }
10354
+ //#endregion
10131
10355
  //#region src/cli/server.ts
10132
10356
  /**
10133
10357
  * `moltnet-agent server` — per-user loopback supervisor (#2061).
@@ -10144,14 +10368,17 @@ async function runAgentServer(argv) {
10144
10368
  console.log(AGENT_SERVER_HELP);
10145
10369
  return 0;
10146
10370
  }
10371
+ const trustRequested = argv[0] === "trust";
10372
+ const commandArgs = trustRequested ? argv.slice(1) : argv;
10147
10373
  const envConfig = loadAgentServerEnvConfig();
10148
10374
  const { values } = parseArgs({
10149
- args: argv,
10375
+ args: commandArgs,
10150
10376
  options: {
10151
10377
  port: { type: "string" },
10152
10378
  "allowed-origins": { type: "string" },
10153
10379
  root: { type: "string" },
10154
- "api-url": { type: "string" }
10380
+ "api-url": { type: "string" },
10381
+ remove: { type: "boolean" }
10155
10382
  }
10156
10383
  });
10157
10384
  const port = Number.parseInt(values.port ?? (envConfig.port || `${DEFAULT_PORT}`), 10);
@@ -10166,6 +10393,7 @@ async function runAgentServer(argv) {
10166
10393
  });
10167
10394
  const defaultApiUrl = values["api-url"] ?? (envConfig.apiUrl || DEFAULT_API_URL);
10168
10395
  const store = new AgentServerStore(root).ensure();
10396
+ if (trustRequested) return runTrustCommand(commandArgs, root);
10169
10397
  const { logger, shutdown: shutdownLogger } = createRootLogger({
10170
10398
  name: "agent-daemon.server",
10171
10399
  level: envConfig.logLevel || "info"
@@ -10193,6 +10421,7 @@ async function runAgentServer(argv) {
10193
10421
  logger,
10194
10422
  runtimeRegistry: new RuntimeRegistry(store.root)
10195
10423
  });
10424
+ const tls = isMacos() ? await ensureTrustedLocalTls(root) : void 0;
10196
10425
  const app = buildAgentServer({
10197
10426
  store,
10198
10427
  secrets,
@@ -10202,7 +10431,11 @@ async function runAgentServer(argv) {
10202
10431
  runs,
10203
10432
  subscriptions,
10204
10433
  allowedOrigins,
10205
- selfOrigin: `http://127.0.0.1:${port}`,
10434
+ selfOrigin: `${tls ? "https" : "http"}://127.0.0.1:${port}`,
10435
+ ...tls ? { tls: {
10436
+ key: tls.key,
10437
+ cert: tls.cert
10438
+ } } : {},
10206
10439
  defaultApiUrl,
10207
10440
  version: "dev",
10208
10441
  logger,
@@ -10238,6 +10471,37 @@ async function runAgentServer(argv) {
10238
10471
  await shutdownLogger();
10239
10472
  }
10240
10473
  }
10474
+ async function runTrustCommand(argv, root) {
10475
+ if (!isMacos()) {
10476
+ console.error("Local HTTPS trust setup is currently supported on macOS only.");
10477
+ return 1;
10478
+ }
10479
+ if (argv.includes("--remove")) {
10480
+ await removeLocalCa(root);
10481
+ console.error("Removed the MoltNet local CA from your login keychain.");
10482
+ return 0;
10483
+ }
10484
+ await ensureTrustedLocalTls(root);
10485
+ console.error("MoltNet local HTTPS trust is ready for this macOS user.");
10486
+ return 0;
10487
+ }
10488
+ async function ensureTrustedLocalTls(root) {
10489
+ const material = await ensureLocalTlsMaterial(root);
10490
+ if (await isLocalCaTrusted(root)) return material;
10491
+ 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.");
10492
+ const prompt = createInterface({
10493
+ input: process.stdin,
10494
+ output: process.stdout
10495
+ });
10496
+ try {
10497
+ const answer = await prompt.question(`Trust MoltNet's local CA (${material.fingerprint}) in this macOS login keychain? [y/N] `);
10498
+ if (!/^y(es)?$/i.test(answer.trim())) throw new Error("Local HTTPS trust was not approved.");
10499
+ } finally {
10500
+ prompt.close();
10501
+ }
10502
+ await trustLocalCa(root);
10503
+ return material;
10504
+ }
10241
10505
  function waitForAgentServerShutdown(runs, app, shutdownController) {
10242
10506
  return new Promise((resolvePromise) => {
10243
10507
  let shuttingDown = false;
@@ -10555,7 +10819,7 @@ async function writeCache(cache) {
10555
10819
  }
10556
10820
  //#endregion
10557
10821
  //#region src/version.ts
10558
- var DAEMON_VERSION = "0.51.0";
10822
+ var DAEMON_VERSION = "0.53.0";
10559
10823
  //#endregion
10560
10824
  //#region src/cli.ts
10561
10825
  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.53.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",
86
- "@themoltnet/agent-runtime": "0.45.2",
87
- "@themoltnet/pi-runtime": "0.14.1",
88
+ "@themoltnet/agent-runtime": "0.45.3",
88
89
  "@themoltnet/os-keyring": "0.3.0",
89
- "@themoltnet/sdk": "0.140.0"
90
+ "@themoltnet/pi-runtime": "0.14.2",
91
+ "@themoltnet/sdk": "0.140.1"
90
92
  },
91
93
  "devDependencies": {
92
94
  "@fastify/swagger": "^9.6.1",
@@ -97,15 +99,15 @@
97
99
  "vite-plugin-dts": "^4.5.4",
98
100
  "vitest": "^3.0.0",
99
101
  "@moltnet/agent-eval": "0.1.0",
100
- "@moltnet/bootstrap": "0.1.0",
101
102
  "@moltnet/crypto-service": "0.1.0",
102
- "@moltnet/execution-plan": "0.1.0",
103
103
  "@moltnet/execution-integrations": "0.1.0",
104
+ "@moltnet/bootstrap": "0.1.0",
105
+ "@moltnet/execution-plan": "0.1.0",
104
106
  "@moltnet/models": "0.1.0",
105
- "@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/loopback-companion": "0.1.0",
109
+ "@moltnet/runtime-profiles": "0.1.0",
110
+ "@moltnet/tasks": "0.1.0"
109
111
  },
110
112
  "nx": {
111
113
  "projectType": "application",