@themoltnet/agent-daemon 0.44.1 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { defaultPiDaemonAdapter } from "./pi.js";
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
4
  import { Type } from "typebox";
5
5
  import "multiformats/cid";
@@ -10,21 +10,11 @@ import { dirname, join, resolve } from "node:path";
10
10
  import { parseArgs, promisify } from "node:util";
11
11
  import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, createLocalSeedSigner, resolveAgentIdentity, resolveProfileWarmSessionTtlSec, resolveRuntimeProfile, resolveRuntimeProfiles, validateRuntimeProfilePrerequisites } from "@themoltnet/agent-runtime";
12
12
  import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets } from "@themoltnet/pi-runtime";
13
+ import { connect, createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
13
14
  import { execFile, execFileSync } from "node:child_process";
14
15
  import { createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, realpathSync, rmSync } from "node:fs";
15
- import { AuthenticationError, MoltNetError, connect, createExecutorAttestor, readConfig } from "@themoltnet/sdk";
16
- import { createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
16
+ import { AuthenticationError, MoltNetError, createExecutorAttestor, readConfig, resolveEnvSecretReference, resolveIdentitySeed } from "@themoltnet/sdk";
17
17
  import { createHash, randomUUID } from "node:crypto";
18
- import * as ed from "@noble/ed25519";
19
- import { createHash as createHash$1, randomBytes } from "crypto";
20
- import "multiformats/codecs/raw";
21
- import "multiformats/hashes/digest";
22
- import "@noble/hashes/sha2";
23
- import "multiformats/bases/base32";
24
- import { ed25519 } from "@noble/curves/ed25519.js";
25
- import "@ipld/dag-cbor";
26
- import "@noble/ciphers/chacha";
27
- import "@noble/hashes/hkdf";
28
18
  import { once } from "node:events";
29
19
  import { pino, transport } from "pino";
30
20
  import { metrics } from "@opentelemetry/api";
@@ -2405,8 +2395,8 @@ async function validateJudgePackInputAsync(input, ctx) {
2405
2395
  //#endregion
2406
2396
  //#region ../../libs/tasks/src/task-types/judge-eval-attempt.ts
2407
2397
  /**
2408
- * `judge_eval_attempt` — score one completed `run_eval` attempt against a
2409
- * hidden judge rubric.
2398
+ * `judge_eval_attempt` — score one completed artifact-producing attempt
2399
+ * against a hidden judge rubric.
2410
2400
  *
2411
2401
  * output_kind: judgment
2412
2402
  * criteria: required (`successCriteria.rubric`)
@@ -2510,9 +2500,9 @@ async function validateJudgeEvalAttemptInputAsync(input, ctx) {
2510
2500
  field: "targetTaskId",
2511
2501
  message: `targetTaskId=${inp.targetTaskId} does not resolve to a task you can read`
2512
2502
  }];
2513
- if (target.taskType !== "run_eval") errors.push({
2503
+ if (target.outputKind !== "artifact") errors.push({
2514
2504
  field: "targetTaskId",
2515
- message: `targetTaskId=${inp.targetTaskId} is a ${target.taskType}, not a run_eval`
2505
+ message: `targetTaskId=${inp.targetTaskId} has outputKind=${target.outputKind}; only artifact-producing tasks can be judged`
2516
2506
  });
2517
2507
  if (!ctx.deferReadinessChecks && (target.status !== "completed" || target.acceptedAttemptN === null)) errors.push({
2518
2508
  field: "targetTaskId",
@@ -2524,7 +2514,7 @@ async function validateJudgeEvalAttemptInputAsync(input, ctx) {
2524
2514
  });
2525
2515
  if (!target.correlationId) errors.push({
2526
2516
  field: "targetTaskId",
2527
- message: "target run_eval has no correlation_id; cannot enforce duplicate-judge protection"
2517
+ message: "target producer has no correlation_id; cannot enforce duplicate-judge protection"
2528
2518
  });
2529
2519
  if (errors.length > 0 || !target.correlationId) return errors;
2530
2520
  const rubric = inp.successCriteria.rubric;
@@ -3433,7 +3423,8 @@ Commands:
3433
3423
  Run \`agent-daemon <command> --help\` for command-specific flags.
3434
3424
 
3435
3425
  Prerequisites:
3436
- - agent-key mode: MOLTNET_AGENT_KEY and MOLTNET_PRIVATE_KEY; no agent files
3426
+ - agent-key mode: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3427
+ MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
3437
3428
  - OAuth2 and sync-sessions: <agent-root>/.moltnet/<agent>/moltnet.json
3438
3429
  - --profile — remote runtime profile supplies provider/model/sandbox
3439
3430
  policy and CWD is used as the VM mountPath.
@@ -3563,7 +3554,8 @@ function isHelpFlag(args) {
3563
3554
  /**
3564
3555
  * Report which auth mode `connect()` will use, without ever reading the secret
3565
3556
  * value into anything logged. Agent-key mode is selected when
3566
- * `MOLTNET_AGENT_KEY` holds a non-blank value — mirroring the SDK precedence
3557
+ * `MOLTNET_AGENT_KEY` or `MOLTNET_AGENT_KEY_REF` holds a non-blank value —
3558
+ * mirroring the SDK precedence
3567
3559
  * where an environment key opts into key mode ahead of the config-file OAuth2
3568
3560
  * credentials. The daemon never passes explicit in-code credentials to
3569
3561
  * `connect()`, so this env-only check matches what `connect()` actually does.
@@ -3571,7 +3563,7 @@ function isHelpFlag(args) {
3571
3563
  * Pure: `env` is passed in (the config module owns the `process.env` read).
3572
3564
  */
3573
3565
  function detectAuthMode(env) {
3574
- return env.MOLTNET_AGENT_KEY?.trim() ? "agent-key" : "oauth2";
3566
+ return env.MOLTNET_AGENT_KEY?.trim() || env.MOLTNET_AGENT_KEY_REF?.trim() ? "agent-key" : "oauth2";
3575
3567
  }
3576
3568
  /**
3577
3569
  * Pure check: may the identity described by `whoami` operate the daemon as
@@ -3637,7 +3629,9 @@ async function resolveAgentContext(agentName, options = {}) {
3637
3629
  return {
3638
3630
  agentDir: join(rootDir, ".moltnet", agentName),
3639
3631
  agentRootDir: rootDir,
3640
- agent: await connect()
3632
+ agent: await connect({ secretProviders: createNodeSecretProviderRegistry() }),
3633
+ credentialSource: "environment",
3634
+ authMechanism: "agent-key"
3641
3635
  };
3642
3636
  }
3643
3637
  const located = locateAgentConfig(roots, agentName);
@@ -3646,10 +3640,13 @@ async function resolveAgentContext(agentName, options = {}) {
3646
3640
  configDir: located.agentDir,
3647
3641
  secretProviders: createNodeSecretProviderRegistry()
3648
3642
  });
3643
+ const config = await readConfig(located.agentDir);
3649
3644
  return {
3650
3645
  agentDir: located.agentDir,
3651
3646
  agentRootDir: located.rootDir,
3652
- agent
3647
+ agent,
3648
+ credentialSource: "config",
3649
+ authMechanism: config?.agent_key_ref ? "agent-key" : "oauth2"
3653
3650
  };
3654
3651
  }
3655
3652
  const tried = roots.map((root) => join(root, ".moltnet", agentName));
@@ -3691,6 +3688,8 @@ function resolveCredentialRoots(agentRootDir) {
3691
3688
  * sprinkling string lookups across the codebase.
3692
3689
  */
3693
3690
  function loadConfig() {
3691
+ assertSingleCredentialForm("MOLTNET_AGENT_KEY", "MOLTNET_AGENT_KEY_REF");
3692
+ assertSingleCredentialForm("MOLTNET_PRIVATE_KEY", "MOLTNET_PRIVATE_KEY_REF");
3694
3693
  return {
3695
3694
  otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
3696
3695
  logLevel: process.env["LOG_LEVEL"] ?? "",
@@ -3699,10 +3698,17 @@ function loadConfig() {
3699
3698
  piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
3700
3699
  authMode: detectAuthMode(process.env),
3701
3700
  signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
3701
+ signingPrivateKeyRef: process.env["MOLTNET_PRIVATE_KEY_REF"] ?? "",
3702
3702
  gitAuthor: process.env["MOLTNET_GIT_AUTHOR"] ?? "",
3703
+ profileCredentialRequirements: process.env["MOLTNET_PROFILE_CREDENTIAL_REQUIREMENTS"] ?? "",
3704
+ credentialBindings: process.env["MOLTNET_CREDENTIAL_BINDINGS"] ?? "",
3705
+ credentialEnforcement: process.env["MOLTNET_CREDENTIAL_ENFORCEMENT"] ?? "",
3703
3706
  traceIdlePolling: readBoolean("MOLTNET_TRACE_IDLE_POLLING", process.env["MOLTNET_TRACE_IDLE_POLLING"])
3704
3707
  };
3705
3708
  }
3709
+ function assertSingleCredentialForm(valueName, refName) {
3710
+ if (process.env[valueName]?.trim() && process.env[refName]?.trim()) throw new Error(`Set only one of ${valueName} or ${refName}`);
3711
+ }
3706
3712
  function readBoolean(name, value) {
3707
3713
  if (value === void 0 || value === "") return false;
3708
3714
  if (value === "true") return true;
@@ -3910,6 +3916,7 @@ function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlS
3910
3916
  return {
3911
3917
  descriptor,
3912
3918
  workspaceMode,
3919
+ workspaceKind: workspaceMode === "scratch_mount" ? "scratch" : void 0,
3913
3920
  sessionKey: slotId,
3914
3921
  slotKey,
3915
3922
  slotId,
@@ -4203,15 +4210,16 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
4203
4210
  if (resolution.kind === "missing") throw new ProducerContextResolutionError(`No live producer runtime slot found for task ${targetTaskId} attempt ${targetAttemptN}`);
4204
4211
  if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has no persisted Pi session path`);
4205
4212
  if (resolution.kind === "remote-session") throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has a durable runtime session but no workspace metadata to copy`);
4213
+ const producerWorkspaceCopySource = resolveProducerWorkspaceCopySource(resolution.producerSlot, stateDirs);
4206
4214
  return {
4207
4215
  ...basePlan,
4208
4216
  workspaceMode: "scratch_mount",
4209
4217
  worktreeBranch: null,
4210
4218
  workspaceKind: "scratch",
4211
- workspaceSeed: {
4212
- copyFromPath: resolveProducerWorkspaceCopySource(resolution.producerSlot, stateDirs),
4219
+ workspaceSeed: producerWorkspaceCopySource ? {
4220
+ copyFromPath: producerWorkspaceCopySource,
4213
4221
  source: "producer"
4214
- },
4222
+ } : null,
4215
4223
  sessionPersistence: {
4216
4224
  sessionDir: `${stateDirs.piSessionsDir}/judge-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`,
4217
4225
  forkFromSessionPath: resolution.sessionPath
@@ -4254,12 +4262,18 @@ function resolveProducerWorkspaceCopySource(producer, stateDirs) {
4254
4262
  if (existsSync(workspacePath)) return workspacePath;
4255
4263
  const recoveredPath = recoverScratchWorkspacePath(producer, stateDirs);
4256
4264
  if (recoveredPath) return recoveredPath;
4265
+ if (isDisposableScratchWorkspace(producer, stateDirs)) return null;
4257
4266
  throw new ProducerContextResolutionError(`Producer workspace path is missing on disk: ${workspacePath}`);
4258
4267
  }
4259
4268
  const sharedMountRoot = dirname(dirname(stateDirs.rootDir));
4260
4269
  if (!existsSync(sharedMountRoot)) throw new ProducerContextResolutionError(`Shared producer mount root is missing on disk: ${sharedMountRoot}`);
4261
4270
  return sharedMountRoot;
4262
4271
  }
4272
+ function isDisposableScratchWorkspace(producer, stateDirs) {
4273
+ if (producer.workspace?.kind === "scratch") return true;
4274
+ if (!producer.workspace?.workspaceId) return false;
4275
+ return producer.workspace.worktreePath === join(stateDirs.rootDir, "task-workspaces", producer.workspace.workspaceId);
4276
+ }
4263
4277
  function recoverScratchWorkspacePath(producer, stateDirs) {
4264
4278
  if (producer.workspace?.worktreeBranch) return null;
4265
4279
  if (!producer.workspace?.workspaceId) return null;
@@ -4267,183 +4281,30 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
4267
4281
  return existsSync(fallback) ? fallback : null;
4268
4282
  }
4269
4283
  //#endregion
4270
- //#region ../../libs/crypto-service/src/ssh.ts
4271
- /**
4272
- * SSH key format conversion for MoltNet Ed25519 keys
4273
- *
4274
- * Converts MoltNet agent keys (ed25519:<base64>) to OpenSSH format
4275
- * for use with git commit signing and SSH authentication.
4276
- */
4277
- if (!ed.etc.sha512Sync) ed.etc.sha512Sync = (...m) => {
4278
- const hash = createHash$1("sha512");
4279
- m.forEach((msg) => hash.update(msg));
4280
- return hash.digest();
4281
- };
4282
- new TextEncoder();
4283
- //#endregion
4284
- //#region ../../libs/crypto-service/src/crypto.service.ts
4285
- /**
4286
- * MoltNet Crypto Service
4287
- *
4288
- * Ed25519 cryptographic operations for agent identity
4289
- * Uses @noble/ed25519 for pure TypeScript implementation
4290
- */
4291
- ed.etc.sha512Sync = (...m) => {
4292
- const hash = createHash$1("sha512");
4293
- m.forEach((msg) => hash.update(msg));
4294
- return hash.digest();
4295
- };
4296
- /** Domain-separation prefix for the signing payload envelope. */
4297
- var DOMAIN_PREFIX = "moltnet:v1";
4298
- /**
4299
- * Build deterministic signing bytes with domain separation and
4300
- * length-prefixed binary framing.
4301
- *
4302
- * Layout:
4303
- * UTF-8("moltnet:v1") || u32be(len(msg_hash)) || msg_hash || u32be(len(nonce_bytes)) || nonce_bytes
4304
- *
4305
- * Where msg_hash = SHA-256(UTF-8(message)).
4306
- *
4307
- * This produces a fixed-structure byte sequence immune to whitespace,
4308
- * newline, and encoding differences between runtimes.
4309
- */
4310
- function buildSigningBytes(message, nonce) {
4311
- const msgHash = createHash$1("sha256").update(Buffer.from(message, "utf-8")).digest();
4312
- const nonceBytes = Buffer.from(nonce, "utf-8");
4313
- const prefix = Buffer.from(DOMAIN_PREFIX, "utf-8");
4314
- const buf = Buffer.alloc(prefix.length + 4 + msgHash.length + 4 + nonceBytes.length);
4315
- let offset = 0;
4316
- prefix.copy(buf, offset);
4317
- offset += prefix.length;
4318
- buf.writeUInt32BE(msgHash.length, offset);
4319
- offset += 4;
4320
- msgHash.copy(buf, offset);
4321
- offset += msgHash.length;
4322
- buf.writeUInt32BE(nonceBytes.length, offset);
4323
- offset += 4;
4324
- nonceBytes.copy(buf, offset);
4325
- return new Uint8Array(buf);
4326
- }
4327
- var cryptoService = {
4328
- async generateKeyPair() {
4329
- const privateKeyBytes = ed.utils.randomPrivateKey();
4330
- const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
4331
- const privateKey = Buffer.from(privateKeyBytes).toString("base64");
4332
- return {
4333
- publicKey: `ed25519:${Buffer.from(publicKeyBytes).toString("base64")}`,
4334
- privateKey,
4335
- fingerprint: this.generateFingerprint(publicKeyBytes)
4336
- };
4337
- },
4338
- generateFingerprint(publicKeyBytes) {
4339
- return (createHash$1("sha256").update(publicKeyBytes).digest("hex").slice(0, 16).toUpperCase().match(/.{4}/g) ?? []).join("-");
4340
- },
4341
- parsePublicKey(publicKey) {
4342
- const base64 = publicKey.replace(/^ed25519:/, "");
4343
- return new Uint8Array(Buffer.from(base64, "base64"));
4344
- },
4345
- async sign(message, privateKeyBase64) {
4346
- const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyBase64, "base64"));
4347
- const messageBytes = new TextEncoder().encode(message);
4348
- const signature = await ed.signAsync(messageBytes, privateKeyBytes);
4349
- return Buffer.from(signature).toString("base64");
4350
- },
4351
- async verify(message, signature, publicKey) {
4352
- try {
4353
- const publicKeyBytes = this.parsePublicKey(publicKey);
4354
- const signatureBytes = new Uint8Array(Buffer.from(signature, "base64"));
4355
- const messageBytes = new TextEncoder().encode(message);
4356
- return await ed.verifyAsync(signatureBytes, messageBytes, publicKeyBytes);
4357
- } catch {
4358
- return false;
4359
- }
4360
- },
4361
- async signWithNonce(message, nonce, privateKeyBase64) {
4362
- const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyBase64, "base64"));
4363
- const signingBytes = buildSigningBytes(message, nonce);
4364
- const signature = await ed.signAsync(signingBytes, privateKeyBytes);
4365
- return Buffer.from(signature).toString("base64");
4366
- },
4367
- async verifyWithNonce(message, nonce, signature, publicKey) {
4368
- try {
4369
- const publicKeyBytes = this.parsePublicKey(publicKey);
4370
- const signatureBytes = new Uint8Array(Buffer.from(signature, "base64"));
4371
- const signingBytes = buildSigningBytes(message, nonce);
4372
- return await ed.verifyAsync(signatureBytes, signingBytes, publicKeyBytes);
4373
- } catch {
4374
- return false;
4375
- }
4376
- },
4377
- async createSignedMessage(message, privateKeyBase64, publicKey) {
4378
- return {
4379
- message,
4380
- signature: await this.sign(message, privateKeyBase64),
4381
- publicKey
4382
- };
4383
- },
4384
- async verifySignedMessage(signedMessage) {
4385
- return this.verify(signedMessage.message, signedMessage.signature, signedMessage.publicKey);
4386
- },
4387
- generateChallenge() {
4388
- return `moltnet:challenge:${randomBytes(32).toString("hex")}:${Date.now()}`;
4389
- },
4390
- async derivePublicKey(privateKeyBase64) {
4391
- const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyBase64, "base64"));
4392
- const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes);
4393
- return `ed25519:${Buffer.from(publicKeyBytes).toString("base64")}`;
4394
- },
4395
- getFingerprintFromPublicKey(publicKey) {
4396
- const publicKeyBytes = this.parsePublicKey(publicKey);
4397
- return this.generateFingerprint(publicKeyBytes);
4398
- },
4399
- deriveX25519PrivateKey(ed25519PrivateKeyBase64) {
4400
- const seed = new Uint8Array(Buffer.from(ed25519PrivateKeyBase64, "base64"));
4401
- const x25519Priv = ed25519.utils.toMontgomerySecret(seed);
4402
- return Buffer.from(x25519Priv).toString("base64");
4403
- },
4404
- deriveX25519PublicKey(ed25519PublicKey) {
4405
- const edPubBytes = this.parsePublicKey(ed25519PublicKey);
4406
- const x25519Pub = ed25519.utils.toMontgomery(edPubBytes);
4407
- return `x25519:${Buffer.from(x25519Pub).toString("base64")}`;
4408
- },
4409
- async createIdentityProof(identityId, privateKeyBase64) {
4410
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4411
- const message = `moltnet:register:${identityId}:${timestamp}`;
4412
- return {
4413
- message,
4414
- signature: await this.sign(message, privateKeyBase64),
4415
- timestamp
4416
- };
4417
- },
4418
- async verifyIdentityProof(proof, publicKey, expectedIdentityId) {
4419
- if (!await this.verify(proof.message, proof.signature, publicKey)) return false;
4420
- const expectedPrefix = `moltnet:register:${expectedIdentityId}:`;
4421
- if (!proof.message.startsWith(expectedPrefix)) return false;
4422
- const proofTime = new Date(proof.timestamp).getTime();
4423
- if (Date.now() - proofTime > 300 * 1e3) return false;
4424
- return true;
4425
- }
4426
- };
4427
- //#endregion
4428
- //#region ../../libs/crypto-service/src/executor-attestation.ts
4429
- ed.etc.sha512Sync = (...m) => {
4430
- const hash = createHash("sha512");
4431
- m.forEach((msg) => hash.update(msg));
4432
- return hash.digest();
4433
- };
4434
- new TextEncoder().encode("SSHSIG");
4435
- //#endregion
4436
4284
  //#region src/lib/executor-attestation.ts
4437
4285
  var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
4438
4286
  async function resolveExecutorSigningPrivateKey(input) {
4439
4287
  if (input.authMode === "agent-key") {
4440
4288
  const privateKey = input.configuredPrivateKey.trim();
4441
- if (!privateKey) throw new Error("Agent-key daemon startup requires MOLTNET_PRIVATE_KEY containing the base64-encoded Ed25519 private key seed.");
4442
- return privateKey;
4289
+ if (privateKey) return privateKey;
4290
+ const reference = input.configuredPrivateKeyRef?.trim();
4291
+ if (!reference) throw new Error("Agent-key daemon startup requires MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF) containing the base64-encoded Ed25519 private key seed.");
4292
+ let resolved;
4293
+ try {
4294
+ resolved = await resolveEnvSecretReference(reference, createNodeSecretProviderRegistry());
4295
+ } catch (cause) {
4296
+ throw new Error(`Agent-key daemon startup could not resolve MOLTNET_PRIVATE_KEY_REF: ${cause.message}`, { cause });
4297
+ }
4298
+ if (Buffer.from(resolved, "base64").length !== 32) throw new Error("MOLTNET_PRIVATE_KEY_REF must resolve to a base64-encoded 32-byte Ed25519 seed.");
4299
+ return resolved;
4300
+ }
4301
+ const config = await readConfig(input.agentDir);
4302
+ if (!config) throw new Error(`OAuth2 daemon startup requires ${input.agentDir}/moltnet.json.`);
4303
+ try {
4304
+ return await resolveIdentitySeed(config, createNodeSecretProviderRegistry());
4305
+ } catch (cause) {
4306
+ throw new Error(`OAuth2 daemon startup could not resolve the signing seed from ${input.agentDir}/moltnet.json (keys.private_key or keys.private_key_ref): ${cause.message}`, { cause });
4443
4307
  }
4444
- const privateKey = (await readConfig(input.agentDir))?.keys?.private_key?.trim();
4445
- if (!privateKey) throw new Error(`OAuth2 daemon startup requires keys.private_key in ${input.agentDir}/moltnet.json.`);
4446
- return privateKey;
4447
4308
  }
4448
4309
  function validateDaemonScopes(whoami) {
4449
4310
  const available = new Set(whoami.scopes ?? []);
@@ -4825,6 +4686,257 @@ async function maybeWriteAnchors(output, ctx) {
4825
4686
  }
4826
4687
  }
4827
4688
  //#endregion
4689
+ //#region ../../libs/execution-integrations/src/credential-broker.ts
4690
+ async function checkCredentialReadiness(requirements, bindings, providers) {
4691
+ const records = [];
4692
+ for (const requirement of requirements) {
4693
+ const binding = ownBinding(bindings, requirement.name);
4694
+ if (binding === void 0) {
4695
+ records.push({
4696
+ name: requirement.name,
4697
+ required: requirement.required,
4698
+ status: requirement.required ? "required_binding_missing" : "binding_absent"
4699
+ });
4700
+ continue;
4701
+ }
4702
+ const bindingDigest = await digestBindingReference(binding.reference);
4703
+ const source = binding.source ?? "local-activation";
4704
+ if (providers.get(binding.reference.provider) === void 0) {
4705
+ records.push({
4706
+ name: requirement.name,
4707
+ required: requirement.required,
4708
+ status: "provider_unavailable",
4709
+ bindingDigest,
4710
+ source
4711
+ });
4712
+ continue;
4713
+ }
4714
+ const probe = await providers.probe(binding.reference);
4715
+ records.push({
4716
+ name: requirement.name,
4717
+ required: requirement.required,
4718
+ status: probe === "present" ? "ready" : probe === "absent" ? "binding_absent" : "host_store_inaccessible",
4719
+ bindingDigest,
4720
+ source
4721
+ });
4722
+ }
4723
+ return records;
4724
+ }
4725
+ async function digestBindingReference(reference) {
4726
+ const bytes = new TextEncoder().encode(`${reference.provider}\u0000${reference.key}`);
4727
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
4728
+ return `sha256:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 16)}`;
4729
+ }
4730
+ function ownBinding(bindings, name) {
4731
+ return Object.hasOwn(bindings, name) ? bindings[name] : void 0;
4732
+ }
4733
+ //#endregion
4734
+ //#region ../../libs/execution-integrations/src/runtime-profile.ts
4735
+ var CURRENT_EFFECTIVE_POLICY_SNAPSHOT_VERSION = "effective-policy:v1";
4736
+ /**
4737
+ * Maps resolved product authority into portable intent. Policy composition is
4738
+ * deliberately upstream: this boundary accepts no policy IDs and performs no
4739
+ * union, lookup, or precedence handling.
4740
+ */
4741
+ function executionIntentFromRuntimeProfile(input) {
4742
+ if (input.policy.snapshot.runtimeKind !== input.profile.runtimeKind) throw new Error("resolved policy authority does not match runtime profile");
4743
+ const network = input.profile.sandboxConfig.network;
4744
+ return {
4745
+ mode: input.mode,
4746
+ profile: {
4747
+ id: input.profile.id,
4748
+ revision: input.profileRevision,
4749
+ definitionCid: input.profile.definitionCid
4750
+ },
4751
+ authority: {
4752
+ policySnapshotHash: input.policy.hash,
4753
+ policySnapshotVersion: input.policy.snapshot.version,
4754
+ ...input.policy.authorizedControls !== void 0 && { authorizedControls: [...input.policy.authorizedControls] }
4755
+ },
4756
+ credentialRequirements: structuredClone(input.credentialRequirements),
4757
+ requiredCapabilities: [...input.requiredCapabilities ?? []],
4758
+ lease: {
4759
+ ttlSec: input.profile.leaseTtlSec,
4760
+ requiredControls: [...input.requiredLeaseControls ?? []]
4761
+ },
4762
+ network: {
4763
+ allowedHosts: [...network?.allowedHosts ?? []],
4764
+ allowedInternalHosts: [...network?.allowedInternalHosts ?? []]
4765
+ },
4766
+ provenance: {
4767
+ profile: input.profile.source,
4768
+ policy: `runtime-policy-snapshot:${input.policy.hash}`,
4769
+ requirements: input.requirementsProvenance
4770
+ }
4771
+ };
4772
+ }
4773
+ //#endregion
4774
+ //#region src/lib/governance-plan.ts
4775
+ /**
4776
+ * Observe-only governance-plan compilation (#1970 private slice).
4777
+ *
4778
+ * Distinct from `task-execution-plan.ts` (workspace/session planning): this
4779
+ * module compiles the CREDENTIAL/CAPABILITY governance plan from trusted
4780
+ * local configuration and logs its value-free decisions. It gates nothing
4781
+ * yet — enforcement is a separate, later change.
4782
+ */
4783
+ var DEFAULT_GOVERNANCE_OBSERVE_TIMEOUT_MS = 2e3;
4784
+ /**
4785
+ * Two distinct provenance sources (#2022 review): requirements come from a
4786
+ * profile-side/private input (`MOLTNET_PROFILE_CREDENTIAL_REQUIREMENTS`, a
4787
+ * JSON map of profile id → requirements) and bindings from trusted
4788
+ * local/deployment configuration (`MOLTNET_CREDENTIAL_BINDINGS`). Both raw
4789
+ * values are read once in config.ts. Both empty → null (observer disabled).
4790
+ * Malformed configuration throws — a truncated credential policy must never
4791
+ * be silently ignored.
4792
+ */
4793
+ function loadRuntimeCredentialConfig(raw) {
4794
+ const hasRequirements = raw.profileRequirements.trim() !== "";
4795
+ const hasBindings = raw.bindings.trim() !== "";
4796
+ if (!hasRequirements && !hasBindings) return null;
4797
+ const requirementsByProfile = {};
4798
+ if (hasRequirements) {
4799
+ const parsed = JSON.parse(raw.profileRequirements);
4800
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("MOLTNET_PROFILE_CREDENTIAL_REQUIREMENTS must be a JSON object keyed by profile id");
4801
+ for (const [profileId, value] of Object.entries(parsed)) requirementsByProfile[profileId] = parseCredentialRequirements(value);
4802
+ }
4803
+ const bindings = {};
4804
+ if (hasBindings) {
4805
+ const parsed = JSON.parse(raw.bindings);
4806
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("MOLTNET_CREDENTIAL_BINDINGS must be a JSON object");
4807
+ for (const [name, binding] of Object.entries(parsed)) {
4808
+ if (typeof binding?.reference?.provider !== "string" || typeof binding.reference.key !== "string") throw new Error(`MOLTNET_CREDENTIAL_BINDINGS binding "${name}" must carry a { reference: { provider, key } }`);
4809
+ bindings[name] = {
4810
+ ...binding,
4811
+ source: "local-bindings-config"
4812
+ };
4813
+ }
4814
+ }
4815
+ return {
4816
+ requirementsByProfile,
4817
+ bindings,
4818
+ sources: {
4819
+ requirements: "profile-requirements-config",
4820
+ bindings: "local-bindings-config"
4821
+ }
4822
+ };
4823
+ }
4824
+ /**
4825
+ * Resolve the credential-governance enforcement mode, mirroring the runtime
4826
+ * tool-policy `off | watch | enforce` ladder. Unset defaults to `watch` when
4827
+ * requirement/binding sources are configured, `off` otherwise. `enforce` is
4828
+ * rejected until the enforcement flip (skip-before-claim) is implemented —
4829
+ * accepting it as a silent watch would misrepresent the deployment.
4830
+ */
4831
+ function resolveCredentialEnforcement(raw, sources) {
4832
+ const configured = sources.profileRequirements.trim() !== "" || sources.bindings.trim() !== "";
4833
+ const value = raw.trim();
4834
+ if (value === "") return configured ? "watch" : "off";
4835
+ if (value === "off" || value === "watch") return value;
4836
+ if (value === "enforce") throw new Error("MOLTNET_CREDENTIAL_ENFORCEMENT=enforce is not implemented yet: the skip-before-claim flip is a separate change. Use watch (audit only).");
4837
+ throw new Error(`MOLTNET_CREDENTIAL_ENFORCEMENT must be off, watch or enforce; got "${value}"`);
4838
+ }
4839
+ async function observeGovernancePlan(input) {
4840
+ const unavailableReason = authorityUnavailableReason(input);
4841
+ if (unavailableReason !== null) {
4842
+ (input.logger.warn ?? input.logger.info).call(input.logger, {
4843
+ mode: "observe",
4844
+ taskId: input.taskId,
4845
+ attemptN: input.attemptN,
4846
+ profileId: input.profile.id,
4847
+ reason: unavailableReason
4848
+ }, "agent-daemon.governance_plan_unavailable");
4849
+ return {
4850
+ status: "unavailable",
4851
+ snapshot: null,
4852
+ reason: unavailableReason
4853
+ };
4854
+ }
4855
+ const { runtimeProfileRevision, policySnapshotHash } = input.claimAuthority;
4856
+ if (runtimeProfileRevision === void 0 || policySnapshotHash === void 0) throw new Error("validated claim authority is missing immutable pins");
4857
+ if (input.offer === void 0) throw new Error("validated execution offer is missing");
4858
+ const requirements = input.config.requirementsByProfile[input.profile.id] ?? [];
4859
+ const credentialReadiness = await checkCredentialReadiness(requirements, input.config.bindings, input.registry);
4860
+ const intent = executionIntentFromRuntimeProfile({
4861
+ mode: "watch",
4862
+ profile: input.profile,
4863
+ profileRevision: runtimeProfileRevision,
4864
+ policy: {
4865
+ hash: policySnapshotHash,
4866
+ snapshot: {
4867
+ version: CURRENT_EFFECTIVE_POLICY_SNAPSHOT_VERSION,
4868
+ runtimeKind: input.profile.runtimeKind
4869
+ },
4870
+ authorizedControls: void 0
4871
+ },
4872
+ credentialRequirements: requirements,
4873
+ requiredCapabilities: [],
4874
+ requirementsProvenance: input.config.sources.requirements
4875
+ });
4876
+ const plan = compileExecutionPlan({
4877
+ intent,
4878
+ offer: input.offer,
4879
+ credentialReadiness
4880
+ });
4881
+ const snapshot = await createExecutionPlanSnapshot({
4882
+ intent,
4883
+ offer: input.offer,
4884
+ credentialReadiness,
4885
+ plan
4886
+ });
4887
+ const logObject = {
4888
+ mode: "observe",
4889
+ taskId: input.taskId,
4890
+ attemptN: input.attemptN,
4891
+ profileId: input.profile.id,
4892
+ sources: input.config.sources,
4893
+ snapshotCid: snapshot.cid,
4894
+ profileRevision: snapshot.intent.profile.revision,
4895
+ policySnapshotHash: snapshot.intent.authority.policySnapshotHash,
4896
+ launchable: plan.launchable,
4897
+ decisions: plan.decisions,
4898
+ credentialReadiness
4899
+ };
4900
+ if (plan.launchable) input.logger.info(logObject, "agent-daemon.governance_plan");
4901
+ else (input.logger.warn ?? input.logger.info).call(input.logger, logObject, "agent-daemon.governance_plan_would_block");
4902
+ return {
4903
+ status: "observed",
4904
+ snapshot
4905
+ };
4906
+ }
4907
+ /**
4908
+ * Observe without allowing a stuck keyring/provider to delay task execution.
4909
+ * The observer is intentionally non-gating: failures and deadlines are logged
4910
+ * with claim correlation and converted to `null`.
4911
+ */
4912
+ async function observeGovernancePlanSafely(input, timeoutMs = DEFAULT_GOVERNANCE_OBSERVE_TIMEOUT_MS) {
4913
+ let timeout;
4914
+ try {
4915
+ return await Promise.race([observeGovernancePlan(input), new Promise((_resolve, reject) => {
4916
+ timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("governance observation timed out")), timeoutMs);
4917
+ })]);
4918
+ } catch (error) {
4919
+ (input.logger.warn ?? input.logger.info).call(input.logger, {
4920
+ mode: "observe",
4921
+ taskId: input.taskId,
4922
+ attemptN: input.attemptN,
4923
+ err: error instanceof Error ? error.message : String(error)
4924
+ }, "agent-daemon.governance_plan_observe_failed");
4925
+ return null;
4926
+ } finally {
4927
+ if (timeout !== void 0) clearTimeout(timeout);
4928
+ }
4929
+ }
4930
+ function authorityUnavailableReason(input) {
4931
+ const authority = input.claimAuthority;
4932
+ if (authority.runtimeProfileId === void 0 || authority.runtimeProfileRevision === void 0 || authority.policySnapshotHash === void 0 || authority.executorFingerprint === void 0) return "claim_authority_unpinned";
4933
+ if (authority.runtimeProfileId !== input.profile.id) return "runtime_profile_mismatch";
4934
+ if (authority.executorFingerprint !== input.executorFingerprint) return "executor_fingerprint_mismatch";
4935
+ if (input.offer === void 0) return "execution_offer_unavailable";
4936
+ if (input.offer.executor.fingerprint !== input.executorFingerprint) return "executor_fingerprint_mismatch";
4937
+ return null;
4938
+ }
4939
+ //#endregion
4828
4940
  //#region src/lib/logger.ts
4829
4941
  /**
4830
4942
  * Logger setup + teardown for the agent-daemon CLI commands.
@@ -5662,6 +5774,11 @@ async function runPolling(opts) {
5662
5774
  }
5663
5775
  if (taskTypes.length === 0) console.error(`[${opts.modeLabel}] --task-types is empty — daemon will accept any registered type. Pass an explicit list to limit scope (e.g. --task-types fulfill_brief).`);
5664
5776
  const cfg = loadConfig();
5777
+ const credentialSources = {
5778
+ profileRequirements: cfg.profileCredentialRequirements,
5779
+ bindings: cfg.credentialBindings
5780
+ };
5781
+ const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
5665
5782
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
5666
5783
  const { ctx, signingPrivateKey, startupWhoami, agentIdentity, hostCapabilitySigner } = await (async () => {
5667
5784
  let gate = "resolve_agent_context";
@@ -5679,7 +5796,8 @@ async function runPolling(opts) {
5679
5796
  const privateKey = await resolveExecutorSigningPrivateKey({
5680
5797
  authMode: cfg.authMode,
5681
5798
  agentDir: resolvedContext.agentDir,
5682
- configuredPrivateKey: cfg.signingPrivateKey
5799
+ configuredPrivateKey: cfg.signingPrivateKey,
5800
+ configuredPrivateKeyRef: cfg.signingPrivateKeyRef
5683
5801
  });
5684
5802
  gate = "validate_scopes";
5685
5803
  validateDaemonScopes(whoami);
@@ -5814,7 +5932,7 @@ async function runPolling(opts) {
5814
5932
  resourceAttributes: {
5815
5933
  "moltnet.team.id": teamId,
5816
5934
  "moltnet.agent.name": baseCommon.agent,
5817
- "moltnet.auth.mode": cfg.authMode,
5935
+ "moltnet.auth.mode": ctx.authMechanism,
5818
5936
  "moltnet.runtime_profile.count": String(profiles.length),
5819
5937
  "moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
5820
5938
  }
@@ -6027,6 +6145,17 @@ async function runPolling(opts) {
6027
6145
  executeTask: async (claimedTask, reporter) => {
6028
6146
  const selected = runtimeForClaimedTask(runtimes, claimedTask);
6029
6147
  const { common, executionPlans, profile, sandbox, slotIdentity, stateDirs } = selected;
6148
+ if (runtimeCredentialConfig) await observeGovernancePlanSafely({
6149
+ config: runtimeCredentialConfig,
6150
+ profile,
6151
+ offer: runtimeExecutionOffer(selected.preparedRuntime, selected.preparedRuntime.attestor.fingerprint),
6152
+ registry: createNodeSecretProviderRegistry(),
6153
+ executorFingerprint: selected.preparedRuntime.attestor.fingerprint,
6154
+ claimAuthority: claimedTask.claimAuthority ?? {},
6155
+ taskId: claimedTask.task.id,
6156
+ attemptN: claimedTask.attemptN,
6157
+ logger: rootLogger
6158
+ });
6030
6159
  const taskLogger = rootLogger.child({
6031
6160
  runtimeProfileId: profile.id,
6032
6161
  runtimeProfileName: profile.name,
@@ -6297,6 +6426,11 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6297
6426
  return 1;
6298
6427
  }
6299
6428
  const cfg = loadConfig();
6429
+ const credentialSources = {
6430
+ profileRequirements: cfg.profileCredentialRequirements,
6431
+ bindings: cfg.credentialBindings
6432
+ };
6433
+ const runtimeCredentialConfig = resolveCredentialEnforcement(cfg.credentialEnforcement, credentialSources) === "off" ? null : loadRuntimeCredentialConfig(credentialSources);
6300
6434
  const initialOpts = opts;
6301
6435
  const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
6302
6436
  const { ctx, signingPrivateKey, agentIdentity, hostCapabilitySigner } = await (async () => {
@@ -6315,7 +6449,8 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6315
6449
  const privateKey = await resolveExecutorSigningPrivateKey({
6316
6450
  authMode: cfg.authMode,
6317
6451
  agentDir: resolvedContext.agentDir,
6318
- configuredPrivateKey: cfg.signingPrivateKey
6452
+ configuredPrivateKey: cfg.signingPrivateKey,
6453
+ configuredPrivateKeyRef: cfg.signingPrivateKeyRef
6319
6454
  });
6320
6455
  gate = "validate_scopes";
6321
6456
  validateDaemonScopes(whoami);
@@ -6415,7 +6550,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6415
6550
  resourceAttributes: {
6416
6551
  "moltnet.task.id": taskId,
6417
6552
  "moltnet.agent.name": opts.agent,
6418
- "moltnet.auth.mode": cfg.authMode,
6553
+ "moltnet.auth.mode": ctx.authMechanism,
6419
6554
  "moltnet.llm.provider": profile.provider,
6420
6555
  "moltnet.llm.model": profile.model,
6421
6556
  ...profile.thinkingLevel ? { "moltnet.llm.thinking_level": profile.thinkingLevel } : {},
@@ -6545,6 +6680,17 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6545
6680
  maxBashTimeouts: opts.maxBashTimeouts
6546
6681
  });
6547
6682
  const executeTask = async (claimedTask, reporter) => {
6683
+ if (runtimeCredentialConfig) await observeGovernancePlanSafely({
6684
+ config: runtimeCredentialConfig,
6685
+ profile,
6686
+ offer: runtimeExecutionOffer(preparedRuntime, preparedRuntime.attestor.fingerprint),
6687
+ registry: createNodeSecretProviderRegistry(),
6688
+ executorFingerprint: preparedRuntime.attestor.fingerprint,
6689
+ claimAuthority: claimedTask.claimAuthority ?? {},
6690
+ taskId: claimedTask.task.id,
6691
+ attemptN: claimedTask.attemptN,
6692
+ logger: rootLogger
6693
+ });
6548
6694
  let executionPlan;
6549
6695
  try {
6550
6696
  executionPlan = await executionPlans.getOrCreate(claimedTask);