@themoltnet/agent-daemon 0.35.0 → 0.37.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 +10 -0
  2. package/dist/cli.js +113 -28
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -94,6 +94,16 @@ fails fast if the key is rejected, is not an agent, or is bound to a different
94
94
  team. See
95
95
  [Run the daemon with an agent key](../../docs/operate/running-agents.md#run-the-daemon-with-an-agent-key).
96
96
 
97
+ An agent key used by the daemon needs this least-privilege scope set:
98
+
99
+ ```text
100
+ agent:profile runtime:read task:read task:claim task:execute
101
+ ```
102
+
103
+ The Console selects these five scopes by default. Broader key, diary, pack,
104
+ team-management, and runtime-management scopes are not required for normal
105
+ daemon operation.
106
+
97
107
  ### Pi provider auth
98
108
 
99
109
  The daemon resolves Pi config from the repository-local `.pi` directory by
package/dist/cli.js CHANGED
@@ -253,7 +253,7 @@ var RUNTIME_PROFILE_CONTEXT_CATALOGUE = {
253
253
  fragments: {
254
254
  "artifact-planner-v1": {
255
255
  binding: "prompt_prefix",
256
- content: "# Bounded artifact planner\n\n- The typed task facts, embedded bounded manifest, exact bound artifact references, registered tools, and runtime capability section are the complete contract. Do not search diaries, inspect a mounted repository, enumerate unrelated tasks or artifacts, use shell commands, modify files, commit, branch, push, or contact GitHub.\n- Read only the exact artifact CIDs named by the task, and only when the embedded manifest does not provide enough evidence. Never paginate or discover artifacts speculatively.\n- Perform semantic classification and planning from supplied content and producer/consumer evidence. Do not substitute filename, directory, language, ecosystem, or repository-specific exclusion rules for evidence.\n- Return exactly the requested versioned structured plan through the registered submit-output tool. Do not emit a second prose or JSON representation.",
256
+ content: "# Bounded artifact planner\n\n- The typed task facts, embedded bounded manifest, exact bound artifact references, registered tools, and runtime capability section are the complete contract. Do not search diaries, inspect a mounted repository, enumerate unrelated tasks or artifacts, modify a checkout, commit, branch, push, or contact GitHub.\n- Read only the exact artifact CIDs named by the task, and only when the embedded manifest does not provide enough evidence. Use the registered task-artifact tools for artifact access; never use shell or CLI wrappers to fetch artifacts, paginate, or discover them speculatively.\n- If the effective runtime exposes a local calculator or shell, use it only inside scratch for coverage accounting, budget arithmetic, and JSON validation. The runtime capability section and policy are authoritative; do not assume a static executable list.\n- Perform semantic classification and planning from supplied content and producer/consumer evidence. Do not substitute filename, directory, language, ecosystem, or repository-specific exclusion rules for evidence.\n- Write and upload exactly the requested versioned plan artifact, then reference its returned metadata through the registered submit-output tool. Do not emit a second prose or JSON representation.",
257
257
  slug: "artifact-planner-v1"
258
258
  },
259
259
  "accountable-delivery-v1": {
@@ -283,7 +283,7 @@ var RUNTIME_PROFILE_CONTEXT_CATALOGUE = {
283
283
  },
284
284
  "verification-and-artifacts-v1": {
285
285
  binding: "prompt_prefix",
286
- content: "# Verification and artifacts\n\n- Run relevant verification before submitting. When task facts include `successCriteria`, assess them honestly in the generated verification contract; a fail or skip with evidence is better than a fabricated pass.\n- The registered submit-output tool owns the exact agent submission schema and validation recovery. Use that schema; do not invent a JSON shape in prose.\n- Upload large files, binary files, logs, reports, screenshots, traces, bundles, or datasets before submitting. Include artifact metadata only where the typed submit contract permits it.\n- If the task depends on prior artifacts, list and download the exact referenced artifact before judging or continuing that work.",
286
+ content: "# Verification and artifacts\n\n- Run relevant verification before submitting. When task facts include `successCriteria`, assess them honestly in the generated verification contract; a fail or skip with evidence is better than a fabricated pass.\n- The registered submit-output tool owns the exact agent submission schema and validation recovery. Use that schema; do not invent a JSON shape in prose.\n- Upload only task-relevant artifacts, and inspect each before uploading. Never upload secrets, credentials, API keys, auth tokens or headers, .env files, or personal or customer data; redact sensitive values, and prefer minimal, sanitized excerpts over whole logs, bundles, or datasets. Include artifact metadata only where the typed submit contract permits it.\n- If the task depends on prior artifacts, list and download the exact referenced artifact before judging or continuing that work.",
287
287
  slug: "verification-and-artifacts-v1"
288
288
  }
289
289
  },
@@ -318,6 +318,46 @@ function deepFreeze(value) {
318
318
  deepFreeze(RUNTIME_PROFILE_CONTEXT_CATALOGUE);
319
319
  Object.freeze(Object.keys(RUNTIME_PROFILE_CONTEXT_CATALOGUE.recipes));
320
320
  //#endregion
321
+ //#region ../../libs/models/src/credential-scopes.ts
322
+ var CREDENTIAL_SCOPES = {
323
+ AgentProfile: "agent:profile",
324
+ ConnectorInvoke: "connector:invoke",
325
+ CryptoSign: "crypto:sign",
326
+ DiaryManage: "diary:manage",
327
+ DiaryRead: "diary:read",
328
+ DiaryWrite: "diary:write",
329
+ HumanProfile: "human:profile",
330
+ KeyManage: "key:manage",
331
+ PackRead: "pack:read",
332
+ PackWrite: "pack:write",
333
+ RuntimeManage: "runtime:manage",
334
+ RuntimeRead: "runtime:read",
335
+ TaskClaim: "task:claim",
336
+ TaskExecute: "task:execute",
337
+ TaskManage: "task:manage",
338
+ TaskRead: "task:read",
339
+ TeamManage: "team:manage",
340
+ TeamRead: "team:read"
341
+ };
342
+ var ALL_CREDENTIAL_SCOPES = Object.freeze(Object.values(CREDENTIAL_SCOPES));
343
+ CREDENTIAL_SCOPES.AgentProfile, CREDENTIAL_SCOPES.RuntimeRead, CREDENTIAL_SCOPES.TaskRead, CREDENTIAL_SCOPES.TaskClaim, CREDENTIAL_SCOPES.TaskExecute;
344
+ Object.freeze(ALL_CREDENTIAL_SCOPES.filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile));
345
+ [
346
+ CREDENTIAL_SCOPES.AgentProfile,
347
+ CREDENTIAL_SCOPES.CryptoSign,
348
+ CREDENTIAL_SCOPES.DiaryManage,
349
+ CREDENTIAL_SCOPES.DiaryRead,
350
+ CREDENTIAL_SCOPES.DiaryWrite,
351
+ CREDENTIAL_SCOPES.HumanProfile,
352
+ CREDENTIAL_SCOPES.PackRead,
353
+ CREDENTIAL_SCOPES.PackWrite,
354
+ CREDENTIAL_SCOPES.TaskExecute,
355
+ CREDENTIAL_SCOPES.TaskManage,
356
+ CREDENTIAL_SCOPES.TaskRead,
357
+ CREDENTIAL_SCOPES.TeamManage,
358
+ CREDENTIAL_SCOPES.TeamRead
359
+ ].filter((scope) => scope !== CREDENTIAL_SCOPES.HumanProfile);
360
+ //#endregion
321
361
  //#region ../../libs/models/src/preview-sign.ts
322
362
  function schemaRef$1(schema, id) {
323
363
  return Type.Unsafe(Type.Ref(id));
@@ -2538,11 +2578,14 @@ var CuratePackOutput = Type.Object({
2538
2578
  //#endregion
2539
2579
  //#region ../../libs/tasks/src/task-types/freeform.ts
2540
2580
  var FREEFORM_TYPE = "freeform";
2541
- var FreeformExecutionOptions = Type.Object({ workspace: Type.Optional(Type.Union([
2542
- Type.Literal("none"),
2543
- Type.Literal("shared_mount"),
2544
- Type.Literal("dedicated_worktree")
2545
- ])) }, {
2581
+ var FreeformExecutionOptions = Type.Object({
2582
+ workspace: Type.Optional(Type.Union([
2583
+ Type.Literal("none"),
2584
+ Type.Literal("shared_mount"),
2585
+ Type.Literal("dedicated_worktree")
2586
+ ])),
2587
+ revision: Type.Optional(Type.String({ pattern: "^[0-9a-fA-F]{40}$" }))
2588
+ }, {
2546
2589
  $id: "FreeformExecutionOptions",
2547
2590
  additionalProperties: false
2548
2591
  });
@@ -2628,6 +2671,12 @@ var FreeformOutput = Type.Object({
2628
2671
  * are sequential preconditions, later ones presume earlier ones hold.
2629
2672
  */
2630
2673
  async function validateFreeformInputAsync(input, ctx) {
2674
+ const execution = input.execution;
2675
+ if (execution?.revision && execution.workspace === "none") return [{
2676
+ field: "input/execution/revision",
2677
+ message: "execution.revision requires a repository workspace; use shared_mount or dedicated_worktree",
2678
+ code: "freeform.executionRevisionRequiresRepository"
2679
+ }];
2631
2680
  const cf = input.continueFrom;
2632
2681
  if (!cf) return [];
2633
2682
  const source = await ctx.resolveTask(cf.taskId);
@@ -2641,11 +2690,16 @@ async function validateFreeformInputAsync(input, ctx) {
2641
2690
  message: `Source task type '${source.taskType}' is not continuable; only freeform → freeform is supported in v1`,
2642
2691
  code: "freeform.sourceTaskTypeNotSupported"
2643
2692
  }];
2644
- if (input.execution?.workspace) return [{
2693
+ if (execution?.workspace) return [{
2645
2694
  field: "input/execution/workspace",
2646
2695
  message: "execution.workspace is derived from parent runtime context when continueFrom is set; omit it",
2647
2696
  code: "freeform.executionWorkspaceNotInheritable"
2648
2697
  }];
2698
+ if (execution?.revision) return [{
2699
+ field: "input/execution/revision",
2700
+ message: "execution.revision is derived from parent runtime context when continueFrom is set; omit it",
2701
+ code: "freeform.executionRevisionNotInheritable"
2702
+ }];
2649
2703
  if (ctx.deferReadinessChecks) return [];
2650
2704
  const attempt = (await ctx.listAttempts(cf.taskId)).find((a) => a.attemptN === cf.attemptN);
2651
2705
  if (!attempt || attempt.status !== "completed") return [{
@@ -4246,6 +4300,7 @@ function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlS
4246
4300
  const slotId = slotKey ? buildDaemonSlotId(identity, slotKey) : null;
4247
4301
  const sessionDir = slotId ? `${stateDirs.piSessionsDir}/${boundedKeyDirComponent(slotId)}` : null;
4248
4302
  const worktreeBranch = resolveTaskWorktreeBranch(task, workspaceMode);
4303
+ const workspaceRevision = resolveTaskWorkspaceRevision(task.input);
4249
4304
  const workspaceId = workspaceMode !== "shared_mount" ? resolveTaskWorkspaceId(task, {
4250
4305
  sessionKey: slotId,
4251
4306
  workspaceScope,
@@ -4261,9 +4316,14 @@ function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlS
4261
4316
  workspaceScope,
4262
4317
  sessionPersistence: sessionDir ? { sessionDir } : null,
4263
4318
  workspaceId,
4264
- worktreeBranch
4319
+ worktreeBranch,
4320
+ workspaceRevision
4265
4321
  };
4266
4322
  }
4323
+ function resolveTaskWorkspaceRevision(input) {
4324
+ const value = input?.execution?.revision;
4325
+ return typeof value === "string" && /^[0-9a-fA-F]{40}$/.test(value) ? value.toLowerCase() : null;
4326
+ }
4267
4327
  function buildDaemonSlotId(identity, slotKey) {
4268
4328
  return [
4269
4329
  "agent",
@@ -4289,6 +4349,7 @@ function slugSlotIdentityComponent(input) {
4289
4349
  }
4290
4350
  function resolveTaskWorktreeBranch(task, workspaceMode) {
4291
4351
  if (workspaceMode !== "dedicated_worktree") return null;
4352
+ if (typeof task.input.execution?.revision === "string") return null;
4292
4353
  if (task.taskType === "fulfill_brief") {
4293
4354
  const input = task.input;
4294
4355
  const slug = slugifyAsciiLower(typeof task.title === "string" && task.title.trim().length > 0 ? task.title : typeof input.brief === "string" && input.brief.trim().length > 0 ? input.brief : task.taskType, 60) || "task";
@@ -4383,9 +4444,14 @@ function createExecutionPlanCache(args) {
4383
4444
  };
4384
4445
  }
4385
4446
  function createNullSourceAttemptResolver() {
4386
- return { findOutputBranch() {
4387
- return Promise.resolve(null);
4388
- } };
4447
+ return {
4448
+ findOutputBranch() {
4449
+ return Promise.resolve(null);
4450
+ },
4451
+ findInputRevision() {
4452
+ return Promise.resolve(null);
4453
+ }
4454
+ };
4389
4455
  }
4390
4456
  function createNullRuntimeSessionStore() {
4391
4457
  return {
@@ -4409,7 +4475,7 @@ function assertPlanAllowedByWorkspacePolicy(plan, policy) {
4409
4475
  }
4410
4476
  function planToRuntimeProfileWorkspaceMode(plan) {
4411
4477
  if (plan.workspaceMode === "scratch_mount") return "none";
4412
- if (plan.workspaceMode === "dedicated_worktree" && !plan.worktreeBranch) return "shared_mount";
4478
+ if (plan.workspaceMode === "dedicated_worktree" && !plan.worktreeBranch && !plan.workspaceRevision) return "shared_mount";
4413
4479
  return plan.workspaceMode;
4414
4480
  }
4415
4481
  function buildClaimedTaskKey(task) {
@@ -4455,16 +4521,20 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
4455
4521
  attemptN: continueFrom.attemptN,
4456
4522
  taskId: continueFrom.taskId
4457
4523
  });
4524
+ const recoveredRevision = recoveredBranch ? null : await sourceAttemptResolver.findInputRevision({
4525
+ attemptN: continueFrom.attemptN,
4526
+ taskId: continueFrom.taskId
4527
+ });
4458
4528
  if (continueFrom.mode === "fork") {
4459
- if (recoveredBranch) {
4529
+ if (recoveredBranch || recoveredRevision) {
4460
4530
  const forkWorkspaceId = `fork-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
4461
- const forkBranch = buildForkBranch(recoveredBranch, claimedTask.task.id, claimedTask.attemptN);
4531
+ const forkBranch = buildForkBranch(recoveredBranch ?? `detached-${continueFrom.taskId.slice(0, 8)}`, claimedTask.task.id, claimedTask.attemptN);
4462
4532
  return {
4463
4533
  ...basePlan,
4464
4534
  workspaceMode: "dedicated_worktree",
4465
4535
  workspaceId: forkWorkspaceId,
4466
4536
  worktreeBranch: forkBranch,
4467
- worktreeBaseRef: recoveredBranch,
4537
+ worktreeBaseRef: recoveredBranch ?? recoveredRevision,
4468
4538
  workspaceKind: "fork",
4469
4539
  sessionPersistence: {
4470
4540
  sessionDir,
@@ -4474,11 +4544,13 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
4474
4544
  }
4475
4545
  throw new ProducerContextResolutionError(`Cannot fork continuation of ${continueFrom.taskId}/${continueFrom.attemptN}: durable runtime session is available but the source attempt output did not report a branch`);
4476
4546
  }
4547
+ const hasDedicatedWorkspace = Boolean(recoveredBranch || recoveredRevision);
4477
4548
  return {
4478
4549
  ...basePlan,
4479
- workspaceMode: "dedicated_worktree",
4480
- workspaceId: recoveredBranch ? buildAttemptWorkspaceId(claimedTask) : null,
4550
+ workspaceMode: hasDedicatedWorkspace ? "dedicated_worktree" : "shared_mount",
4551
+ workspaceId: hasDedicatedWorkspace ? buildAttemptWorkspaceId(claimedTask) : null,
4481
4552
  worktreeBranch: recoveredBranch,
4553
+ workspaceRevision: recoveredRevision,
4482
4554
  sessionPersistence: {
4483
4555
  sessionDir,
4484
4556
  forkFromSessionPath: resolution.sessionPath
@@ -4486,16 +4558,20 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
4486
4558
  };
4487
4559
  }
4488
4560
  const parentBranch = resolution.producerSlot.workspace?.worktreeBranch ?? null;
4561
+ const parentRevision = parentBranch ? null : await sourceAttemptResolver.findInputRevision({
4562
+ attemptN: continueFrom.attemptN,
4563
+ taskId: continueFrom.taskId
4564
+ });
4489
4565
  if (continueFrom.mode === "fork") {
4490
- if (!parentBranch) throw new ProducerContextResolutionError(`Cannot fork continuation of ${continueFrom.taskId}/${continueFrom.attemptN}: producer slot has no worktree branch to fork from`);
4566
+ if (!parentBranch && !parentRevision) throw new ProducerContextResolutionError(`Cannot fork continuation of ${continueFrom.taskId}/${continueFrom.attemptN}: producer slot has no worktree branch or immutable revision to fork from`);
4491
4567
  const forkWorkspaceId = `fork-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
4492
- const forkBranch = buildForkBranch(parentBranch, claimedTask.task.id, claimedTask.attemptN);
4568
+ const forkBranch = buildForkBranch(parentBranch ?? `detached-${continueFrom.taskId.slice(0, 8)}`, claimedTask.task.id, claimedTask.attemptN);
4493
4569
  return {
4494
4570
  ...basePlan,
4495
4571
  workspaceMode: "dedicated_worktree",
4496
4572
  workspaceId: forkWorkspaceId,
4497
4573
  worktreeBranch: forkBranch,
4498
- worktreeBaseRef: parentBranch,
4574
+ worktreeBaseRef: parentBranch ?? parentRevision,
4499
4575
  workspaceKind: "fork",
4500
4576
  sessionPersistence: {
4501
4577
  sessionDir,
@@ -4503,11 +4579,13 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
4503
4579
  }
4504
4580
  };
4505
4581
  }
4582
+ const hasDedicatedWorkspace = Boolean(parentBranch || parentRevision);
4506
4583
  return {
4507
4584
  ...basePlan,
4508
- workspaceMode: "dedicated_worktree",
4509
- workspaceId: parentBranch ? buildAttemptWorkspaceId(claimedTask) : null,
4585
+ workspaceMode: hasDedicatedWorkspace ? "dedicated_worktree" : "shared_mount",
4586
+ workspaceId: hasDedicatedWorkspace ? buildAttemptWorkspaceId(claimedTask) : null,
4510
4587
  worktreeBranch: parentBranch,
4588
+ workspaceRevision: parentRevision,
4511
4589
  sessionPersistence: {
4512
4590
  sessionDir,
4513
4591
  forkFromSessionPath: resolution.sessionPath
@@ -5702,11 +5780,18 @@ function signalExitCode(signal) {
5702
5780
  //#region src/lib/source-attempts.ts
5703
5781
  function createApiSourceAttemptResolver(args) {
5704
5782
  const { agent } = args;
5705
- return { async findOutputBranch(input) {
5706
- const attempt = (await agent.tasks.listAttempts(input.taskId)).find((candidate) => candidate.attemptN === input.attemptN);
5707
- if (!attempt || attempt.status !== "completed") return null;
5708
- return resolveOutputBranch(attempt.output);
5709
- } };
5783
+ return {
5784
+ async findOutputBranch(input) {
5785
+ const attempt = (await agent.tasks.listAttempts(input.taskId)).find((candidate) => candidate.attemptN === input.attemptN);
5786
+ if (!attempt || attempt.status !== "completed") return null;
5787
+ return resolveOutputBranch(attempt.output);
5788
+ },
5789
+ async findInputRevision(input) {
5790
+ const task = await agent.tasks.get(input.taskId);
5791
+ if (task.status !== "completed" || task.acceptedAttemptN !== input.attemptN) return null;
5792
+ return resolveTaskWorkspaceRevision(task.input);
5793
+ }
5794
+ };
5710
5795
  }
5711
5796
  function resolveOutputBranch(output) {
5712
5797
  if (!output || typeof output !== "object") return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.35.0",
3
+ "version": "0.37.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.",
@@ -62,9 +62,9 @@
62
62
  "pino": "^10.3.1",
63
63
  "pino-pretty": "^13.1.3",
64
64
  "typebox": "^1.2.8",
65
- "@themoltnet/agent-runtime": "0.39.1",
66
- "@themoltnet/pi-runtime": "0.5.0",
67
- "@themoltnet/sdk": "0.128.0"
65
+ "@themoltnet/agent-runtime": "0.40.1",
66
+ "@themoltnet/sdk": "0.129.0",
67
+ "@themoltnet/pi-runtime": "0.6.1"
68
68
  },
69
69
  "devDependencies": {
70
70
  "tsx": "^4.7.0",