@themoltnet/agent-daemon 0.44.1 → 0.45.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 +1 -1
  2. package/dist/cli.js +59 -23
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -91,7 +91,7 @@ All config flows from environment variables. The daemon reads them in
91
91
 
92
92
  For OAuth2/local mode, the agent's `moltnet.json` and gitconfig live next to
93
93
  each other in `.moltnet/<agent>/`. Provision them once via
94
- [`legreffier init`](../../docs/start/install-and-initialize.md).
94
+ [`moltnet agents init`](../../docs/start/install-and-initialize.md#initialize-an-autonomous-agent).
95
95
 
96
96
  **Auth mode.** When `MOLTNET_AGENT_KEY` is set the daemon authenticates with
97
97
  that key as an opaque bearer token (no OAuth2 exchange); otherwise it uses the
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource, cre
12
12
  import { GuestEnvironmentBoundaryError, assertGuestEnvironmentBoundary, createPiRetryTriage, findMainWorktree, isResolvedPathInsideRoot, normalizeRetryTriageResult, redactRetryTriageSecrets } from "@themoltnet/pi-runtime";
13
13
  import { execFile, execFileSync } from "node:child_process";
14
14
  import { createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, realpathSync, rmSync } from "node:fs";
15
- import { AuthenticationError, MoltNetError, connect, createExecutorAttestor, readConfig } from "@themoltnet/sdk";
15
+ import { AuthenticationError, MoltNetError, connect, createExecutorAttestor, readConfig, resolveEnvSecretReference, resolveIdentitySeed } from "@themoltnet/sdk";
16
16
  import { createNodeSecretProviderRegistry } from "@themoltnet/sdk/node";
17
17
  import { createHash, randomUUID } from "node:crypto";
18
18
  import * as ed from "@noble/ed25519";
@@ -2405,8 +2405,8 @@ async function validateJudgePackInputAsync(input, ctx) {
2405
2405
  //#endregion
2406
2406
  //#region ../../libs/tasks/src/task-types/judge-eval-attempt.ts
2407
2407
  /**
2408
- * `judge_eval_attempt` — score one completed `run_eval` attempt against a
2409
- * hidden judge rubric.
2408
+ * `judge_eval_attempt` — score one completed artifact-producing attempt
2409
+ * against a hidden judge rubric.
2410
2410
  *
2411
2411
  * output_kind: judgment
2412
2412
  * criteria: required (`successCriteria.rubric`)
@@ -2510,9 +2510,9 @@ async function validateJudgeEvalAttemptInputAsync(input, ctx) {
2510
2510
  field: "targetTaskId",
2511
2511
  message: `targetTaskId=${inp.targetTaskId} does not resolve to a task you can read`
2512
2512
  }];
2513
- if (target.taskType !== "run_eval") errors.push({
2513
+ if (target.outputKind !== "artifact") errors.push({
2514
2514
  field: "targetTaskId",
2515
- message: `targetTaskId=${inp.targetTaskId} is a ${target.taskType}, not a run_eval`
2515
+ message: `targetTaskId=${inp.targetTaskId} has outputKind=${target.outputKind}; only artifact-producing tasks can be judged`
2516
2516
  });
2517
2517
  if (!ctx.deferReadinessChecks && (target.status !== "completed" || target.acceptedAttemptN === null)) errors.push({
2518
2518
  field: "targetTaskId",
@@ -2524,7 +2524,7 @@ async function validateJudgeEvalAttemptInputAsync(input, ctx) {
2524
2524
  });
2525
2525
  if (!target.correlationId) errors.push({
2526
2526
  field: "targetTaskId",
2527
- message: "target run_eval has no correlation_id; cannot enforce duplicate-judge protection"
2527
+ message: "target producer has no correlation_id; cannot enforce duplicate-judge protection"
2528
2528
  });
2529
2529
  if (errors.length > 0 || !target.correlationId) return errors;
2530
2530
  const rubric = inp.successCriteria.rubric;
@@ -3433,7 +3433,8 @@ Commands:
3433
3433
  Run \`agent-daemon <command> --help\` for command-specific flags.
3434
3434
 
3435
3435
  Prerequisites:
3436
- - agent-key mode: MOLTNET_AGENT_KEY and MOLTNET_PRIVATE_KEY; no agent files
3436
+ - agent-key mode: MOLTNET_AGENT_KEY (or MOLTNET_AGENT_KEY_REF) and
3437
+ MOLTNET_PRIVATE_KEY (or MOLTNET_PRIVATE_KEY_REF); no agent files
3437
3438
  - OAuth2 and sync-sessions: <agent-root>/.moltnet/<agent>/moltnet.json
3438
3439
  - --profile — remote runtime profile supplies provider/model/sandbox
3439
3440
  policy and CWD is used as the VM mountPath.
@@ -3563,7 +3564,8 @@ function isHelpFlag(args) {
3563
3564
  /**
3564
3565
  * Report which auth mode `connect()` will use, without ever reading the secret
3565
3566
  * value into anything logged. Agent-key mode is selected when
3566
- * `MOLTNET_AGENT_KEY` holds a non-blank value — mirroring the SDK precedence
3567
+ * `MOLTNET_AGENT_KEY` or `MOLTNET_AGENT_KEY_REF` holds a non-blank value —
3568
+ * mirroring the SDK precedence
3567
3569
  * where an environment key opts into key mode ahead of the config-file OAuth2
3568
3570
  * credentials. The daemon never passes explicit in-code credentials to
3569
3571
  * `connect()`, so this env-only check matches what `connect()` actually does.
@@ -3571,7 +3573,7 @@ function isHelpFlag(args) {
3571
3573
  * Pure: `env` is passed in (the config module owns the `process.env` read).
3572
3574
  */
3573
3575
  function detectAuthMode(env) {
3574
- return env.MOLTNET_AGENT_KEY?.trim() ? "agent-key" : "oauth2";
3576
+ return env.MOLTNET_AGENT_KEY?.trim() || env.MOLTNET_AGENT_KEY_REF?.trim() ? "agent-key" : "oauth2";
3575
3577
  }
3576
3578
  /**
3577
3579
  * Pure check: may the identity described by `whoami` operate the daemon as
@@ -3637,7 +3639,9 @@ async function resolveAgentContext(agentName, options = {}) {
3637
3639
  return {
3638
3640
  agentDir: join(rootDir, ".moltnet", agentName),
3639
3641
  agentRootDir: rootDir,
3640
- agent: await connect()
3642
+ agent: await connect({ secretProviders: createNodeSecretProviderRegistry() }),
3643
+ credentialSource: "environment",
3644
+ authMechanism: "agent-key"
3641
3645
  };
3642
3646
  }
3643
3647
  const located = locateAgentConfig(roots, agentName);
@@ -3646,10 +3650,13 @@ async function resolveAgentContext(agentName, options = {}) {
3646
3650
  configDir: located.agentDir,
3647
3651
  secretProviders: createNodeSecretProviderRegistry()
3648
3652
  });
3653
+ const config = await readConfig(located.agentDir);
3649
3654
  return {
3650
3655
  agentDir: located.agentDir,
3651
3656
  agentRootDir: located.rootDir,
3652
- agent
3657
+ agent,
3658
+ credentialSource: "config",
3659
+ authMechanism: config?.agent_key_ref ? "agent-key" : "oauth2"
3653
3660
  };
3654
3661
  }
3655
3662
  const tried = roots.map((root) => join(root, ".moltnet", agentName));
@@ -3691,6 +3698,8 @@ function resolveCredentialRoots(agentRootDir) {
3691
3698
  * sprinkling string lookups across the codebase.
3692
3699
  */
3693
3700
  function loadConfig() {
3701
+ assertSingleCredentialForm("MOLTNET_AGENT_KEY", "MOLTNET_AGENT_KEY_REF");
3702
+ assertSingleCredentialForm("MOLTNET_PRIVATE_KEY", "MOLTNET_PRIVATE_KEY_REF");
3694
3703
  return {
3695
3704
  otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
3696
3705
  logLevel: process.env["LOG_LEVEL"] ?? "",
@@ -3699,10 +3708,14 @@ function loadConfig() {
3699
3708
  piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? "",
3700
3709
  authMode: detectAuthMode(process.env),
3701
3710
  signingPrivateKey: process.env["MOLTNET_PRIVATE_KEY"] ?? "",
3711
+ signingPrivateKeyRef: process.env["MOLTNET_PRIVATE_KEY_REF"] ?? "",
3702
3712
  gitAuthor: process.env["MOLTNET_GIT_AUTHOR"] ?? "",
3703
3713
  traceIdlePolling: readBoolean("MOLTNET_TRACE_IDLE_POLLING", process.env["MOLTNET_TRACE_IDLE_POLLING"])
3704
3714
  };
3705
3715
  }
3716
+ function assertSingleCredentialForm(valueName, refName) {
3717
+ if (process.env[valueName]?.trim() && process.env[refName]?.trim()) throw new Error(`Set only one of ${valueName} or ${refName}`);
3718
+ }
3706
3719
  function readBoolean(name, value) {
3707
3720
  if (value === void 0 || value === "") return false;
3708
3721
  if (value === "true") return true;
@@ -3910,6 +3923,7 @@ function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlS
3910
3923
  return {
3911
3924
  descriptor,
3912
3925
  workspaceMode,
3926
+ workspaceKind: workspaceMode === "scratch_mount" ? "scratch" : void 0,
3913
3927
  sessionKey: slotId,
3914
3928
  slotKey,
3915
3929
  slotId,
@@ -4203,15 +4217,16 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
4203
4217
  if (resolution.kind === "missing") throw new ProducerContextResolutionError(`No live producer runtime slot found for task ${targetTaskId} attempt ${targetAttemptN}`);
4204
4218
  if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has no persisted Pi session path`);
4205
4219
  if (resolution.kind === "remote-session") throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has a durable runtime session but no workspace metadata to copy`);
4220
+ const producerWorkspaceCopySource = resolveProducerWorkspaceCopySource(resolution.producerSlot, stateDirs);
4206
4221
  return {
4207
4222
  ...basePlan,
4208
4223
  workspaceMode: "scratch_mount",
4209
4224
  worktreeBranch: null,
4210
4225
  workspaceKind: "scratch",
4211
- workspaceSeed: {
4212
- copyFromPath: resolveProducerWorkspaceCopySource(resolution.producerSlot, stateDirs),
4226
+ workspaceSeed: producerWorkspaceCopySource ? {
4227
+ copyFromPath: producerWorkspaceCopySource,
4213
4228
  source: "producer"
4214
- },
4229
+ } : null,
4215
4230
  sessionPersistence: {
4216
4231
  sessionDir: `${stateDirs.piSessionsDir}/judge-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`,
4217
4232
  forkFromSessionPath: resolution.sessionPath
@@ -4254,12 +4269,18 @@ function resolveProducerWorkspaceCopySource(producer, stateDirs) {
4254
4269
  if (existsSync(workspacePath)) return workspacePath;
4255
4270
  const recoveredPath = recoverScratchWorkspacePath(producer, stateDirs);
4256
4271
  if (recoveredPath) return recoveredPath;
4272
+ if (isDisposableScratchWorkspace(producer, stateDirs)) return null;
4257
4273
  throw new ProducerContextResolutionError(`Producer workspace path is missing on disk: ${workspacePath}`);
4258
4274
  }
4259
4275
  const sharedMountRoot = dirname(dirname(stateDirs.rootDir));
4260
4276
  if (!existsSync(sharedMountRoot)) throw new ProducerContextResolutionError(`Shared producer mount root is missing on disk: ${sharedMountRoot}`);
4261
4277
  return sharedMountRoot;
4262
4278
  }
4279
+ function isDisposableScratchWorkspace(producer, stateDirs) {
4280
+ if (producer.workspace?.kind === "scratch") return true;
4281
+ if (!producer.workspace?.workspaceId) return false;
4282
+ return producer.workspace.worktreePath === join(stateDirs.rootDir, "task-workspaces", producer.workspace.workspaceId);
4283
+ }
4263
4284
  function recoverScratchWorkspacePath(producer, stateDirs) {
4264
4285
  if (producer.workspace?.worktreeBranch) return null;
4265
4286
  if (!producer.workspace?.workspaceId) return null;
@@ -4438,12 +4459,25 @@ var DAEMON_REQUIRED_SCOPES = AGENT_CREDENTIAL_SCOPES;
4438
4459
  async function resolveExecutorSigningPrivateKey(input) {
4439
4460
  if (input.authMode === "agent-key") {
4440
4461
  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;
4462
+ if (privateKey) return privateKey;
4463
+ const reference = input.configuredPrivateKeyRef?.trim();
4464
+ 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.");
4465
+ let resolved;
4466
+ try {
4467
+ resolved = await resolveEnvSecretReference(reference, createNodeSecretProviderRegistry());
4468
+ } catch (cause) {
4469
+ throw new Error(`Agent-key daemon startup could not resolve MOLTNET_PRIVATE_KEY_REF: ${cause.message}`, { cause });
4470
+ }
4471
+ if (Buffer.from(resolved, "base64").length !== 32) throw new Error("MOLTNET_PRIVATE_KEY_REF must resolve to a base64-encoded 32-byte Ed25519 seed.");
4472
+ return resolved;
4473
+ }
4474
+ const config = await readConfig(input.agentDir);
4475
+ if (!config) throw new Error(`OAuth2 daemon startup requires ${input.agentDir}/moltnet.json.`);
4476
+ try {
4477
+ return await resolveIdentitySeed(config, createNodeSecretProviderRegistry());
4478
+ } catch (cause) {
4479
+ 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
4480
  }
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
4481
  }
4448
4482
  function validateDaemonScopes(whoami) {
4449
4483
  const available = new Set(whoami.scopes ?? []);
@@ -5679,7 +5713,8 @@ async function runPolling(opts) {
5679
5713
  const privateKey = await resolveExecutorSigningPrivateKey({
5680
5714
  authMode: cfg.authMode,
5681
5715
  agentDir: resolvedContext.agentDir,
5682
- configuredPrivateKey: cfg.signingPrivateKey
5716
+ configuredPrivateKey: cfg.signingPrivateKey,
5717
+ configuredPrivateKeyRef: cfg.signingPrivateKeyRef
5683
5718
  });
5684
5719
  gate = "validate_scopes";
5685
5720
  validateDaemonScopes(whoami);
@@ -5814,7 +5849,7 @@ async function runPolling(opts) {
5814
5849
  resourceAttributes: {
5815
5850
  "moltnet.team.id": teamId,
5816
5851
  "moltnet.agent.name": baseCommon.agent,
5817
- "moltnet.auth.mode": cfg.authMode,
5852
+ "moltnet.auth.mode": ctx.authMechanism,
5818
5853
  "moltnet.runtime_profile.count": String(profiles.length),
5819
5854
  "moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
5820
5855
  }
@@ -6315,7 +6350,8 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6315
6350
  const privateKey = await resolveExecutorSigningPrivateKey({
6316
6351
  authMode: cfg.authMode,
6317
6352
  agentDir: resolvedContext.agentDir,
6318
- configuredPrivateKey: cfg.signingPrivateKey
6353
+ configuredPrivateKey: cfg.signingPrivateKey,
6354
+ configuredPrivateKeyRef: cfg.signingPrivateKeyRef
6319
6355
  });
6320
6356
  gate = "validate_scopes";
6321
6357
  validateDaemonScopes(whoami);
@@ -6415,7 +6451,7 @@ async function runOnce(argv, runtimeAdapter = defaultPiDaemonAdapter) {
6415
6451
  resourceAttributes: {
6416
6452
  "moltnet.task.id": taskId,
6417
6453
  "moltnet.agent.name": opts.agent,
6418
- "moltnet.auth.mode": cfg.authMode,
6454
+ "moltnet.auth.mode": ctx.authMechanism,
6419
6455
  "moltnet.llm.provider": profile.provider,
6420
6456
  "moltnet.llm.model": profile.model,
6421
6457
  ...profile.thinkingLevel ? { "moltnet.llm.thinking_level": profile.thinkingLevel } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.44.1",
3
+ "version": "0.45.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.",
@@ -78,10 +78,10 @@
78
78
  "pino-opentelemetry-transport": "^3.0.0",
79
79
  "pino-pretty": "^13.1.3",
80
80
  "typebox": "^1.2.8",
81
- "@themoltnet/agent-runtime": "0.44.0",
82
- "@themoltnet/os-keyring": "0.2.0",
83
- "@themoltnet/pi-runtime": "0.12.1",
84
- "@themoltnet/sdk": "0.137.0"
81
+ "@themoltnet/os-keyring": "0.3.0",
82
+ "@themoltnet/agent-runtime": "0.45.0",
83
+ "@themoltnet/pi-runtime": "0.12.2",
84
+ "@themoltnet/sdk": "0.138.0"
85
85
  },
86
86
  "devDependencies": {
87
87
  "tsx": "^4.7.0",