@themoltnet/agent-daemon 0.16.0 → 0.17.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 +5 -0
  2. package/dist/main.js +852 -238
  3. package/package.json +9 -8
package/dist/main.js CHANGED
@@ -9,10 +9,10 @@ import { PinoInstrumentation } from "@opentelemetry/instrumentation-pino";
9
9
  import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
10
10
  import crypto$1, { createHash } from "crypto";
11
11
  import { createHash as createHash$1 } from "node:crypto";
12
- import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
+ import path, { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
13
  import { parseArgs, parseEnv, promisify } from "node:util";
14
14
  import { execFile, execFileSync } from "node:child_process";
15
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
15
+ import { accessSync, constants, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
16
16
  import { DatabaseSync } from "node:sqlite";
17
17
  import { ROOT_CONTEXT, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
18
18
  import { pino, transport } from "pino";
@@ -836,8 +836,8 @@ Reset();
836
836
  *
837
837
  * Idempotent: registration is guarded by `Format.Has(...)`.
838
838
  */
839
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
840
- if (!Has("uuid")) Set$1("uuid", (v) => UUID_RE.test(v));
839
+ var UUID_RE$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
840
+ if (!Has("uuid")) Set$1("uuid", (v) => UUID_RE$1.test(v));
841
841
  if (!Has("date-time")) Set$1("date-time", (v) => !Number.isNaN(Date.parse(v)));
842
842
  //#endregion
843
843
  //#region ../../node_modules/.pnpm/typebox@1.2.8/node_modules/typebox/build/system/memory/metrics.mjs
@@ -4273,18 +4273,120 @@ var TaskContext = _Array_(_Object_({
4273
4273
  maxItems: 5
4274
4274
  });
4275
4275
  //#endregion
4276
- //#region ../../libs/tasks/src/daemon-profiles.ts
4277
- var DaemonProfileName = String$1({
4276
+ //#region ../../libs/tasks/src/rubric.ts
4277
+ /**
4278
+ * Rubric — structured acceptance criteria used by judgment tasks.
4279
+ *
4280
+ * Phase 1 (this PR): rubrics are embedded in task inputs. Their integrity
4281
+ * is pinned via the task's `input_cid` (which covers the whole input,
4282
+ * including the inline rubric). No separate storage, no CRUD.
4283
+ *
4284
+ * Phase 2 (see #881): rubrics become a first-class resource with their
4285
+ * own signed rows and CIDv1 lookup. The schema below is designed to
4286
+ * carry forward unchanged — only storage and addressing differ.
4287
+ *
4288
+ * Until Phase 2 lands, `rubricId` + `version` + `contentHash` are
4289
+ * informational fields the author fills in; no uniqueness is enforced.
4290
+ * `contentHash` is optional in Phase 1 because the *task*'s input_cid
4291
+ * is the authoritative commitment.
4292
+ */
4293
+ /**
4294
+ * How a judge must score a single criterion.
4295
+ *
4296
+ * - `llm_score`: 0..1 continuous, `rationale` required. Smooths failures
4297
+ * into the gradient — use `llm_checklist` instead for properties where
4298
+ * a single failure is a real failure (grounding, faithfulness).
4299
+ * - `llm_checklist`: judge enumerates per-claim assertions with
4300
+ * `{passed, evidence}`. The criterion's numeric `score` is derived:
4301
+ * `1` iff every assertion passes, else `0`. Per-claim evidence is the
4302
+ * dataset for cluster-analysis of failure modes. See #999.
4303
+ * - `boolean`: 0 or 1, `rationale` optional.
4304
+ * - `deterministic_signature_check`: judge runs a signature check;
4305
+ * result is 0 or 1. No LLM discretion.
4306
+ * - `deterministic_coverage_check`: every referenced source entry
4307
+ * appears in the rendered output; 0 or 1.
4308
+ */
4309
+ var RubricScoringMode = Union([
4310
+ Literal("llm_score"),
4311
+ Literal("llm_checklist"),
4312
+ Literal("boolean"),
4313
+ Literal("deterministic_signature_check"),
4314
+ Literal("deterministic_coverage_check")
4315
+ ], { $id: "RubricScoringMode" });
4316
+ /**
4317
+ * One binary check produced by an `llm_checklist`-mode criterion.
4318
+ *
4319
+ * `evidence` is REQUIRED for both PASS and FAIL — agentskills.io grading
4320
+ * principle: \"Don't give the benefit of the doubt.\" A PASS without
4321
+ * concrete evidence (a quoted span, an entry id, a source location)
4322
+ * cannot be audited. A FAIL without evidence cannot be clustered into
4323
+ * structural fixes. The same shape is reused by `judge-eval-variant`
4324
+ * (#943) so tooling, dashboards, and analysis stay uniform.
4325
+ */
4326
+ var AssertionResult = _Object_({
4327
+ id: String$1({ minLength: 1 }),
4328
+ text: String$1({ minLength: 1 }),
4329
+ passed: Boolean$1(),
4330
+ evidence: String$1({ minLength: 1 })
4331
+ }, {
4332
+ $id: "AssertionResult",
4333
+ additionalProperties: false
4334
+ });
4335
+ var RubricCriterion = _Object_({
4336
+ id: String$1({ minLength: 1 }),
4337
+ description: String$1({ minLength: 1 }),
4338
+ weight: Number$1({
4339
+ minimum: 0,
4340
+ maximum: 1
4341
+ }),
4342
+ scoring: RubricScoringMode
4343
+ }, {
4344
+ $id: "RubricCriterion",
4345
+ additionalProperties: false
4346
+ });
4347
+ /**
4348
+ * A complete rubric. Same shape used in Phase 1 (inline) and Phase 2
4349
+ * (stored row `body`); only the addressing mechanism differs.
4350
+ */
4351
+ var Rubric = _Object_({
4352
+ rubricId: String$1({ minLength: 1 }),
4353
+ version: String$1({ minLength: 1 }),
4354
+ preamble: Optional(String$1()),
4355
+ criteria: _Array_(RubricCriterion, { minItems: 1 }),
4356
+ scope: Optional(String$1()),
4357
+ contentHash: Optional(String$1())
4358
+ }, {
4359
+ $id: "Rubric",
4360
+ additionalProperties: false
4361
+ });
4362
+ /**
4363
+ * Verify rubric criteria weights sum to 1.0 within floating-point tolerance.
4364
+ * The schema constrains each weight to [0,1] but can't express a cross-field
4365
+ * sum constraint, so this is enforced programmatically by callers that
4366
+ * accept rubrics (task input validators, server-side task creation).
4367
+ *
4368
+ * Returns null when valid; otherwise an error message suitable for surfacing
4369
+ * to the caller. Tolerance is 1e-6 to accommodate JSON round-tripping of
4370
+ * decimal fractions (e.g. 0.1 + 0.2 + 0.3 + 0.4 ≠ 1.0 exactly).
4371
+ */
4372
+ function validateRubricWeights(rubric) {
4373
+ const sum = rubric.criteria.reduce((acc, c) => acc + c.weight, 0);
4374
+ if (Math.abs(sum - 1) > 1e-6) return `Rubric weights must sum to 1.0 (got ${sum.toFixed(6)})`;
4375
+ return null;
4376
+ }
4377
+ //#endregion
4378
+ //#region ../../libs/tasks/src/runtime-profiles.ts
4379
+ var RuntimeProfileName = String$1({
4278
4380
  minLength: 1,
4279
4381
  maxLength: 100,
4280
4382
  pattern: "^[a-zA-Z0-9][a-zA-Z0-9_-]{0,99}$"
4281
4383
  });
4282
- var DaemonProfileEnvName = String$1({
4384
+ var RuntimeProfileEnvName = String$1({
4283
4385
  minLength: 1,
4284
4386
  maxLength: 128,
4285
4387
  pattern: "^[A-Z_][A-Z0-9_]*$"
4286
4388
  });
4287
- var DaemonProfileToolName = String$1({
4389
+ var RuntimeProfileToolName = String$1({
4288
4390
  minLength: 1,
4289
4391
  maxLength: 128,
4290
4392
  pattern: "^[a-zA-Z0-9._/-]+$"
@@ -4297,7 +4399,7 @@ var SandboxResumeCommandWhenSchema = _Object_({ workspaceMode: Optional(_Array_(
4297
4399
  minItems: 1,
4298
4400
  maxItems: 3
4299
4401
  })) }, { additionalProperties: false });
4300
- var DaemonProfileSandboxResumeCommand = Union([String$1({
4402
+ var RuntimeProfileSandboxResumeCommand = Union([String$1({
4301
4403
  minLength: 1,
4302
4404
  maxLength: 4096
4303
4405
  }), _Object_({
@@ -4315,7 +4417,7 @@ var DaemonProfileSandboxResumeCommand = Union([String$1({
4315
4417
  maximum: 6e4
4316
4418
  }))
4317
4419
  }, { additionalProperties: false })]);
4318
- var DaemonProfileSandbox = _Object_({
4420
+ var RuntimeProfileSandbox = _Object_({
4319
4421
  snapshot: Optional(_Object_({
4320
4422
  setupCommands: Optional(_Array_(String$1({
4321
4423
  minLength: 1,
@@ -4331,7 +4433,7 @@ var DaemonProfileSandbox = _Object_({
4331
4433
  pattern: "^[0-9]+[KMGTP]?$"
4332
4434
  }))
4333
4435
  }, { additionalProperties: false })),
4334
- resumeCommands: Optional(_Array_(DaemonProfileSandboxResumeCommand, { maxItems: 30 })),
4436
+ resumeCommands: Optional(_Array_(RuntimeProfileSandboxResumeCommand, { maxItems: 30 })),
4335
4437
  vfs: Optional(_Object_({
4336
4438
  shadow: Optional(_Array_(String$1({
4337
4439
  minLength: 1,
@@ -4339,7 +4441,7 @@ var DaemonProfileSandbox = _Object_({
4339
4441
  }), { maxItems: 100 })),
4340
4442
  shadowMode: Optional(Union([Literal("deny"), Literal("tmpfs")]))
4341
4443
  }, { additionalProperties: false })),
4342
- env: Optional(Record(DaemonProfileEnvName, String$1({ maxLength: 4096 }))),
4444
+ env: Optional(Record(RuntimeProfileEnvName, String$1({ maxLength: 4096 }))),
4343
4445
  hostExec: Optional(_Object_({ autoApprove: Optional(Literal(false)) }, { additionalProperties: false })),
4344
4446
  resources: Optional(_Object_({
4345
4447
  memory: Optional(String$1({
@@ -4353,10 +4455,10 @@ var DaemonProfileSandbox = _Object_({
4353
4455
  }))
4354
4456
  }, { additionalProperties: false }))
4355
4457
  }, {
4356
- $id: "DaemonProfileSandbox",
4458
+ $id: "RuntimeProfileSandbox",
4357
4459
  additionalProperties: false
4358
4460
  });
4359
- var DaemonProfileContext = _Object_({
4461
+ var RuntimeProfileContext = _Object_({
4360
4462
  slug: String$1({
4361
4463
  minLength: 1,
4362
4464
  maxLength: 64,
@@ -4373,17 +4475,29 @@ var DaemonProfileContext = _Object_({
4373
4475
  maxLength: 65536
4374
4476
  })
4375
4477
  }, {
4376
- $id: "DaemonProfileContext",
4478
+ $id: "RuntimeProfileContext",
4377
4479
  additionalProperties: false
4378
4480
  });
4379
- var DaemonProfileRef = _Object_({ profileId: String$1({ format: "uuid" }) }, {
4380
- $id: "DaemonProfileRef",
4481
+ var RuntimeProfileRef = _Object_({ profileId: String$1({ format: "uuid" }) }, {
4482
+ $id: "RuntimeProfileRef",
4381
4483
  additionalProperties: false
4382
4484
  });
4485
+ var RuntimeProfileLeaseTtlSec = Integer({
4486
+ minimum: 1,
4487
+ maximum: 86400
4488
+ });
4489
+ var RuntimeProfileHeartbeatIntervalMs = Integer({
4490
+ minimum: 0,
4491
+ maximum: 36e5
4492
+ });
4493
+ var RuntimeProfileMaxBatchSize = Integer({
4494
+ minimum: 1,
4495
+ maximum: 1e3
4496
+ });
4383
4497
  _Object_({
4384
4498
  id: String$1({ format: "uuid" }),
4385
4499
  teamId: String$1({ format: "uuid" }),
4386
- name: DaemonProfileName,
4500
+ name: RuntimeProfileName,
4387
4501
  description: Union([String$1({ maxLength: 4096 }), Null()]),
4388
4502
  provider: String$1({
4389
4503
  minLength: 1,
@@ -4394,7 +4508,7 @@ _Object_({
4394
4508
  maxLength: 200
4395
4509
  }),
4396
4510
  runtimeKind: Literal("gondolin_pi"),
4397
- sandbox: DaemonProfileSandbox,
4511
+ sandbox: RuntimeProfileSandbox,
4398
4512
  sessionStorageMode: Literal("local"),
4399
4513
  workspaceStorageMode: Literal("local"),
4400
4514
  sessionTtlSec: Integer({
@@ -4405,9 +4519,12 @@ _Object_({
4405
4519
  minimum: 1,
4406
4520
  maximum: 86400
4407
4521
  }),
4408
- requiredEnv: _Array_(DaemonProfileEnvName, { maxItems: 100 }),
4409
- requiredTools: _Array_(DaemonProfileToolName, { maxItems: 100 }),
4410
- context: _Array_(DaemonProfileContext, { maxItems: 5 }),
4522
+ leaseTtlSec: RuntimeProfileLeaseTtlSec,
4523
+ heartbeatIntervalMs: RuntimeProfileHeartbeatIntervalMs,
4524
+ maxBatchSize: RuntimeProfileMaxBatchSize,
4525
+ requiredEnv: _Array_(RuntimeProfileEnvName, { maxItems: 100 }),
4526
+ requiredTools: _Array_(RuntimeProfileToolName, { maxItems: 100 }),
4527
+ context: _Array_(RuntimeProfileContext, { maxItems: 5 }),
4411
4528
  revision: Integer({ minimum: 1 }),
4412
4529
  definitionCid: String$1({
4413
4530
  minLength: 1,
@@ -4418,111 +4535,9 @@ _Object_({
4418
4535
  createdAt: String$1({ format: "date-time" }),
4419
4536
  updatedAt: String$1({ format: "date-time" })
4420
4537
  }, {
4421
- $id: "DaemonProfile",
4422
- additionalProperties: false
4423
- });
4424
- //#endregion
4425
- //#region ../../libs/tasks/src/rubric.ts
4426
- /**
4427
- * Rubric — structured acceptance criteria used by judgment tasks.
4428
- *
4429
- * Phase 1 (this PR): rubrics are embedded in task inputs. Their integrity
4430
- * is pinned via the task's `input_cid` (which covers the whole input,
4431
- * including the inline rubric). No separate storage, no CRUD.
4432
- *
4433
- * Phase 2 (see #881): rubrics become a first-class resource with their
4434
- * own signed rows and CIDv1 lookup. The schema below is designed to
4435
- * carry forward unchanged — only storage and addressing differ.
4436
- *
4437
- * Until Phase 2 lands, `rubricId` + `version` + `contentHash` are
4438
- * informational fields the author fills in; no uniqueness is enforced.
4439
- * `contentHash` is optional in Phase 1 because the *task*'s input_cid
4440
- * is the authoritative commitment.
4441
- */
4442
- /**
4443
- * How a judge must score a single criterion.
4444
- *
4445
- * - `llm_score`: 0..1 continuous, `rationale` required. Smooths failures
4446
- * into the gradient — use `llm_checklist` instead for properties where
4447
- * a single failure is a real failure (grounding, faithfulness).
4448
- * - `llm_checklist`: judge enumerates per-claim assertions with
4449
- * `{passed, evidence}`. The criterion's numeric `score` is derived:
4450
- * `1` iff every assertion passes, else `0`. Per-claim evidence is the
4451
- * dataset for cluster-analysis of failure modes. See #999.
4452
- * - `boolean`: 0 or 1, `rationale` optional.
4453
- * - `deterministic_signature_check`: judge runs a signature check;
4454
- * result is 0 or 1. No LLM discretion.
4455
- * - `deterministic_coverage_check`: every referenced source entry
4456
- * appears in the rendered output; 0 or 1.
4457
- */
4458
- var RubricScoringMode = Union([
4459
- Literal("llm_score"),
4460
- Literal("llm_checklist"),
4461
- Literal("boolean"),
4462
- Literal("deterministic_signature_check"),
4463
- Literal("deterministic_coverage_check")
4464
- ], { $id: "RubricScoringMode" });
4465
- /**
4466
- * One binary check produced by an `llm_checklist`-mode criterion.
4467
- *
4468
- * `evidence` is REQUIRED for both PASS and FAIL — agentskills.io grading
4469
- * principle: \"Don't give the benefit of the doubt.\" A PASS without
4470
- * concrete evidence (a quoted span, an entry id, a source location)
4471
- * cannot be audited. A FAIL without evidence cannot be clustered into
4472
- * structural fixes. The same shape is reused by `judge-eval-variant`
4473
- * (#943) so tooling, dashboards, and analysis stay uniform.
4474
- */
4475
- var AssertionResult = _Object_({
4476
- id: String$1({ minLength: 1 }),
4477
- text: String$1({ minLength: 1 }),
4478
- passed: Boolean$1(),
4479
- evidence: String$1({ minLength: 1 })
4480
- }, {
4481
- $id: "AssertionResult",
4482
- additionalProperties: false
4483
- });
4484
- var RubricCriterion = _Object_({
4485
- id: String$1({ minLength: 1 }),
4486
- description: String$1({ minLength: 1 }),
4487
- weight: Number$1({
4488
- minimum: 0,
4489
- maximum: 1
4490
- }),
4491
- scoring: RubricScoringMode
4492
- }, {
4493
- $id: "RubricCriterion",
4538
+ $id: "RuntimeProfile",
4494
4539
  additionalProperties: false
4495
4540
  });
4496
- /**
4497
- * A complete rubric. Same shape used in Phase 1 (inline) and Phase 2
4498
- * (stored row `body`); only the addressing mechanism differs.
4499
- */
4500
- var Rubric = _Object_({
4501
- rubricId: String$1({ minLength: 1 }),
4502
- version: String$1({ minLength: 1 }),
4503
- preamble: Optional(String$1()),
4504
- criteria: _Array_(RubricCriterion, { minItems: 1 }),
4505
- scope: Optional(String$1()),
4506
- contentHash: Optional(String$1())
4507
- }, {
4508
- $id: "Rubric",
4509
- additionalProperties: false
4510
- });
4511
- /**
4512
- * Verify rubric criteria weights sum to 1.0 within floating-point tolerance.
4513
- * The schema constrains each weight to [0,1] but can't express a cross-field
4514
- * sum constraint, so this is enforced programmatically by callers that
4515
- * accept rubrics (task input validators, server-side task creation).
4516
- *
4517
- * Returns null when valid; otherwise an error message suitable for surfacing
4518
- * to the caller. Tolerance is 1e-6 to accommodate JSON round-tripping of
4519
- * decimal fractions (e.g. 0.1 + 0.2 + 0.3 + 0.4 ≠ 1.0 exactly).
4520
- */
4521
- function validateRubricWeights(rubric) {
4522
- const sum = rubric.criteria.reduce((acc, c) => acc + c.weight, 0);
4523
- if (Math.abs(sum - 1) > 1e-6) return `Rubric weights must sum to 1.0 (got ${sum.toFixed(6)})`;
4524
- return null;
4525
- }
4526
4541
  //#endregion
4527
4542
  //#region ../../libs/tasks/src/success-criteria.ts
4528
4543
  /**
@@ -9116,6 +9131,7 @@ var TaskAttemptStatus = Union([
9116
9131
  Literal("completed"),
9117
9132
  Literal("failed"),
9118
9133
  Literal("cancelled"),
9134
+ Literal("aborted"),
9119
9135
  Literal("timed_out")
9120
9136
  ], { $id: "TaskAttemptStatus" });
9121
9137
  var ExecutorTrustLevel = Union([
@@ -9268,7 +9284,7 @@ _Object_({
9268
9284
  acceptedAttemptN: Union([Number$1(), Null()]),
9269
9285
  claimCondition: Union([Unsafe(Ref$2("ClaimCondition")), Null()]),
9270
9286
  requiredExecutorTrustLevel: ExecutorTrustLevel,
9271
- allowedProfiles: _Array_(DaemonProfileRef, { maxItems: 16 }),
9287
+ allowedProfiles: _Array_(RuntimeProfileRef, { maxItems: 16 }),
9272
9288
  status: TaskStatus,
9273
9289
  queuedAt: IsoTimestamp,
9274
9290
  completedAt: Union([IsoTimestamp, Null()]),
@@ -9364,15 +9380,23 @@ _Object_({
9364
9380
  //#region src/lib/help.ts
9365
9381
  var COMMON_REQUIRED_FLAGS = `\
9366
9382
  -a, --agent <name> MoltNet agent identity. Reads credentials
9367
- from <repo-root>/.moltnet/<name>/moltnet.json.
9383
+ from <repo-root>/.moltnet/<name>/moltnet.json.`;
9384
+ var COMMON_MODEL_FLAGS = `\
9368
9385
  -p, --provider <id> LLM provider id (e.g. anthropic, openai-codex).
9369
9386
  -m, --model <id> LLM model id for the provider (e.g.
9370
- claude-sonnet-4-5, gpt-5.3-codex).`;
9387
+ claude-sonnet-4-5, gpt-5.3-codex). Required
9388
+ unless --profile is set.`;
9371
9389
  var COMMON_OPTIONAL_FLAGS = `\
9372
9390
  --sandbox <path> Path to sandbox.json. Default: search up from
9373
9391
  the daemon's CWD until found. The directory
9374
9392
  containing sandbox.json is also used as the
9375
- VM mountPath.
9393
+ VM mountPath. Cannot be used with --profile.
9394
+ --profile <uuid|name> Remote runtime profile. When set, provider,
9395
+ model, and sandbox policy come from the
9396
+ profile; task listing/claiming is restricted
9397
+ to unrestricted tasks plus tasks allowing this
9398
+ profile. requiredEnv/requiredTools are checked
9399
+ before claiming. Name lookup is team-scoped.
9376
9400
  --lease-ttl-sec <n> Sliding liveness window. Silence longer than
9377
9401
  this ends the attempt with lease_expired.
9378
9402
  Default: 300.
@@ -9390,7 +9414,8 @@ var COMMON_OPTIONAL_FLAGS = `\
9390
9414
  --warm-session-ttl-sec <n> Retain resumable daemon slots (Pi sessions +
9391
9415
  reusable worktrees) in local daemon state for
9392
9416
  this many seconds after use. 0 = disable reuse.
9393
- Default: 1800.
9417
+ Default: 1800, or min(profile session/workspace
9418
+ TTL) when --profile is set.
9394
9419
  --debug Verbose logging: also log successful list/claim
9395
9420
  outcomes (candidate counts, claim attempts).`;
9396
9421
  var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
@@ -9413,9 +9438,10 @@ Run \`agent-daemon <command> --help\` for command-specific flags.
9413
9438
 
9414
9439
  Prerequisites (all subcommands):
9415
9440
  - <repo-root>/.moltnet/<agent>/moltnet.json — credentials (see --agent)
9416
- - sandbox.json — Gondolin snapshot config; resolved by searching up
9417
- from CWD, or pass --sandbox <path>. Its containing directory is the
9418
- VM mountPath.
9441
+ - sandbox.json or --profile — local sandbox config is resolved by
9442
+ searching up from CWD, or pass --sandbox <path>. With --profile, the
9443
+ remote runtime profile supplies provider/model/sandbox policy and CWD
9444
+ is used as the VM mountPath.
9419
9445
 
9420
9446
  Registered task types: ${knownTaskTypesList()}`;
9421
9447
  var POLL_HELP = `\
@@ -9428,6 +9454,7 @@ Required:
9428
9454
  --team <uuid> Team whose queue to serve. The daemon must be
9429
9455
  a member of this team (canAccessTeam permit).
9430
9456
  ${COMMON_REQUIRED_FLAGS}
9457
+ ${COMMON_MODEL_FLAGS}
9431
9458
 
9432
9459
  Optional:
9433
9460
  --task-types <csv> Whitelist of task types to claim. Default:
@@ -9458,6 +9485,7 @@ Required:
9458
9485
  -t, --task-id <uuid> Task to claim and execute. Must already be
9459
9486
  in 'queued' status.
9460
9487
  ${COMMON_REQUIRED_FLAGS}
9488
+ ${COMMON_MODEL_FLAGS}
9461
9489
 
9462
9490
  Optional:
9463
9491
  ${COMMON_OPTIONAL_FLAGS}
@@ -9483,6 +9511,7 @@ sleeps and retries forever).
9483
9511
  Required:
9484
9512
  --team <uuid> Team whose queue to drain.
9485
9513
  ${COMMON_REQUIRED_FLAGS}
9514
+ ${COMMON_MODEL_FLAGS}
9486
9515
 
9487
9516
  Optional:
9488
9517
  --task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
@@ -24459,6 +24488,9 @@ var ApiTaskReporter = class {
24459
24488
  get cancelReason() {
24460
24489
  return this.observedCancelReason;
24461
24490
  }
24491
+ requestCancel(reason) {
24492
+ this.abortForCancel(reason);
24493
+ }
24462
24494
  async open(ctx) {
24463
24495
  if (this.heartbeatTimer) {
24464
24496
  clearInterval(this.heartbeatTimer);
@@ -24641,13 +24673,15 @@ var ApiTaskReporter = class {
24641
24673
  async sendHeartbeat() {
24642
24674
  const body = this.opts.leaseTtlSec ? { leaseTtlSec: this.opts.leaseTtlSec } : {};
24643
24675
  const response = await this.opts.tasks.heartbeat(this.taskId, this.attemptN, body);
24644
- if (response?.cancelled && !this.cancelController.signal.aborted) {
24645
- this.observedCancelReason = response.cancelReason ?? null;
24646
- this.cancelController.abort(/* @__PURE__ */ new Error(`Task cancelled by proposer${this.observedCancelReason ? `: ${this.observedCancelReason}` : ""}`));
24647
- if (this.heartbeatTimer) {
24648
- clearInterval(this.heartbeatTimer);
24649
- this.heartbeatTimer = null;
24650
- }
24676
+ if (response?.cancelled) this.abortForCancel(response.cancelReason ?? null);
24677
+ }
24678
+ abortForCancel(reason) {
24679
+ if (this.cancelController.signal.aborted) return;
24680
+ this.observedCancelReason = reason;
24681
+ this.cancelController.abort(/* @__PURE__ */ new Error(`Task cancelled${this.observedCancelReason ? `: ${this.observedCancelReason}` : ""}`));
24682
+ if (this.heartbeatTimer) {
24683
+ clearInterval(this.heartbeatTimer);
24684
+ this.heartbeatTimer = null;
24651
24685
  }
24652
24686
  }
24653
24687
  };
@@ -24660,6 +24694,7 @@ var AgentRuntime = class {
24660
24694
  currentTaskId: null
24661
24695
  };
24662
24696
  stopRequested = false;
24697
+ currentReporter = null;
24663
24698
  logger;
24664
24699
  constructor(opts) {
24665
24700
  this.opts = opts;
@@ -24688,6 +24723,7 @@ var AgentRuntime = class {
24688
24723
  });
24689
24724
  taskLogger.info({}, "agent-runtime.task_claimed");
24690
24725
  const reporter = this.opts.makeReporter(claimedTask);
24726
+ this.currentReporter = reporter;
24691
24727
  const taskCtx = Object.keys(claimedTask.traceHeaders).length ? propagation.extract(ROOT_CONTEXT, claimedTask.traceHeaders) : context.active();
24692
24728
  const taskStart = Date.now();
24693
24729
  let output;
@@ -24745,16 +24781,19 @@ var AgentRuntime = class {
24745
24781
  }
24746
24782
  this.status.tasksProcessed += 1;
24747
24783
  this.status.currentTaskId = null;
24784
+ this.currentReporter = null;
24748
24785
  }
24749
24786
  } finally {
24787
+ this.currentReporter = null;
24750
24788
  await this.opts.source.close();
24751
24789
  this.status.state = "stopped";
24752
24790
  }
24753
24791
  return outputs;
24754
24792
  }
24755
24793
  /** Request cooperative shutdown. Safe from signal handlers. */
24756
- stop() {
24794
+ stop(reason) {
24757
24795
  this.stopRequested = true;
24796
+ if (reason !== void 0) this.currentReporter?.requestCancel?.(reason);
24758
24797
  }
24759
24798
  };
24760
24799
  //#endregion
@@ -24766,8 +24805,11 @@ var ApiTaskSource = class {
24766
24805
  }
24767
24806
  async claim() {
24768
24807
  if (this.claimed) return null;
24769
- const { agent, taskId, leaseTtlSec } = this.opts;
24770
- const result = await agent.tasks.claim(taskId, leaseTtlSec ? { leaseTtlSec } : {});
24808
+ const { agent, taskId, leaseTtlSec, profileId } = this.opts;
24809
+ const result = await agent.tasks.claim(taskId, {
24810
+ ...leaseTtlSec ? { leaseTtlSec } : {},
24811
+ ...profileId ? { profileId } : {}
24812
+ });
24771
24813
  this.claimed = true;
24772
24814
  return {
24773
24815
  task: result.task,
@@ -26412,6 +26454,124 @@ var updateRenderedPack = (options) => (options.client ?? client).patch({
26412
26454
  }
26413
26455
  });
26414
26456
  /**
26457
+ * List runtime profiles for the active team context.
26458
+ */
26459
+ var listRuntimeProfiles = (options) => (options?.client ?? client).get({
26460
+ security: [
26461
+ {
26462
+ scheme: "bearer",
26463
+ type: "http"
26464
+ },
26465
+ {
26466
+ name: "X-Moltnet-Session-Token",
26467
+ type: "apiKey"
26468
+ },
26469
+ {
26470
+ in: "cookie",
26471
+ name: "ory_kratos_session",
26472
+ type: "apiKey"
26473
+ }
26474
+ ],
26475
+ url: "/runtime-profiles",
26476
+ ...options
26477
+ });
26478
+ /**
26479
+ * Create a runtime profile for the active team context.
26480
+ */
26481
+ var createRuntimeProfile = (options) => (options?.client ?? client).post({
26482
+ security: [
26483
+ {
26484
+ scheme: "bearer",
26485
+ type: "http"
26486
+ },
26487
+ {
26488
+ name: "X-Moltnet-Session-Token",
26489
+ type: "apiKey"
26490
+ },
26491
+ {
26492
+ in: "cookie",
26493
+ name: "ory_kratos_session",
26494
+ type: "apiKey"
26495
+ }
26496
+ ],
26497
+ url: "/runtime-profiles",
26498
+ ...options,
26499
+ headers: {
26500
+ "Content-Type": "application/json",
26501
+ ...options?.headers
26502
+ }
26503
+ });
26504
+ /**
26505
+ * Delete one runtime profile.
26506
+ */
26507
+ var deleteRuntimeProfile = (options) => (options.client ?? client).delete({
26508
+ security: [
26509
+ {
26510
+ scheme: "bearer",
26511
+ type: "http"
26512
+ },
26513
+ {
26514
+ name: "X-Moltnet-Session-Token",
26515
+ type: "apiKey"
26516
+ },
26517
+ {
26518
+ in: "cookie",
26519
+ name: "ory_kratos_session",
26520
+ type: "apiKey"
26521
+ }
26522
+ ],
26523
+ url: "/runtime-profiles/{profileId}",
26524
+ ...options
26525
+ });
26526
+ /**
26527
+ * Get one runtime profile.
26528
+ */
26529
+ var getRuntimeProfile = (options) => (options.client ?? client).get({
26530
+ security: [
26531
+ {
26532
+ scheme: "bearer",
26533
+ type: "http"
26534
+ },
26535
+ {
26536
+ name: "X-Moltnet-Session-Token",
26537
+ type: "apiKey"
26538
+ },
26539
+ {
26540
+ in: "cookie",
26541
+ name: "ory_kratos_session",
26542
+ type: "apiKey"
26543
+ }
26544
+ ],
26545
+ url: "/runtime-profiles/{profileId}",
26546
+ ...options
26547
+ });
26548
+ /**
26549
+ * Update one runtime profile.
26550
+ */
26551
+ var updateRuntimeProfile = (options) => (options.client ?? client).patch({
26552
+ security: [
26553
+ {
26554
+ scheme: "bearer",
26555
+ type: "http"
26556
+ },
26557
+ {
26558
+ name: "X-Moltnet-Session-Token",
26559
+ type: "apiKey"
26560
+ },
26561
+ {
26562
+ in: "cookie",
26563
+ name: "ory_kratos_session",
26564
+ type: "apiKey"
26565
+ }
26566
+ ],
26567
+ url: "/runtime-profiles/{profileId}",
26568
+ ...options,
26569
+ headers: {
26570
+ "Content-Type": "application/json",
26571
+ ...options.headers
26572
+ }
26573
+ });
26574
+ /**
26415
26575
  * List tasks for a team with optional filters.
26416
26576
  */
26417
26577
  var listTasks = (options) => (options.client ?? client).get({
@@ -26526,6 +26686,32 @@ var listTaskAttempts = (options) => (options.client ?? client).get({
26526
26686
  ...options
26527
26687
  });
26528
26688
  /**
26689
+ * Claimant intentionally abandons this attempt (e.g. daemon shutdown). The attempt becomes aborted and the task requeues for another claim (or fails when retries are exhausted). Does NOT cancel the task.
26690
+ */
26691
+ var abortTaskAttempt = (options) => (options.client ?? client).post({
26692
+ security: [
26693
+ {
26694
+ scheme: "bearer",
26695
+ type: "http"
26696
+ },
26697
+ {
26698
+ name: "X-Moltnet-Session-Token",
26699
+ type: "apiKey"
26700
+ },
26701
+ {
26702
+ in: "cookie",
26703
+ name: "ory_kratos_session",
26704
+ type: "apiKey"
26705
+ }
26706
+ ],
26707
+ url: "/tasks/{id}/attempts/{n}/abort",
26708
+ ...options,
26709
+ headers: {
26710
+ "Content-Type": "application/json",
26711
+ ...options.headers
26712
+ }
26713
+ });
26714
+ /**
26529
26715
  * Mark an attempt as completed with output.
26530
26716
  */
26531
26717
  var completeTask = (options) => (options.client ?? client).post({
@@ -28658,6 +28844,54 @@ function createRecoveryNamespace(context) {
28658
28844
  };
28659
28845
  }
28660
28846
  //#endregion
28847
+ //#region ../../libs/sdk/src/namespaces/runtime-profiles.ts
28848
+ function createRuntimeProfilesNamespace(context) {
28849
+ const { client, auth } = context;
28850
+ return {
28851
+ async list(options) {
28852
+ return unwrapResult(await listRuntimeProfiles({
28853
+ client,
28854
+ auth,
28855
+ headers: teamHeaders(options)
28856
+ }));
28857
+ },
28858
+ async create(body, options) {
28859
+ return unwrapResult(await createRuntimeProfile({
28860
+ client,
28861
+ auth,
28862
+ headers: teamHeaders(options),
28863
+ body
28864
+ }));
28865
+ },
28866
+ async get(profileId) {
28867
+ return unwrapResult(await getRuntimeProfile({
28868
+ client,
28869
+ auth,
28870
+ path: { profileId }
28871
+ }));
28872
+ },
28873
+ async update(profileId, body) {
28874
+ return unwrapResult(await updateRuntimeProfile({
28875
+ client,
28876
+ auth,
28877
+ path: { profileId },
28878
+ body
28879
+ }));
28880
+ },
28881
+ async delete(profileId) {
28882
+ const result = await deleteRuntimeProfile({
28883
+ client,
28884
+ auth,
28885
+ path: { profileId }
28886
+ });
28887
+ if (result.error) unwrapResult(result);
28888
+ }
28889
+ };
28890
+ }
28891
+ function teamHeaders(options) {
28892
+ return options?.teamId ? { "x-moltnet-team-id": options.teamId } : void 0;
28893
+ }
28894
+ //#endregion
28661
28895
  //#region ../../libs/sdk/src/namespaces/signing-requests.ts
28662
28896
  function createSigningRequestsNamespace(context) {
28663
28897
  const { client, auth } = context;
@@ -28778,6 +29012,17 @@ function createTasksNamespace(context) {
28778
29012
  body
28779
29013
  }));
28780
29014
  },
29015
+ async abortAttempt(id, n, body) {
29016
+ return unwrapResult(await abortTaskAttempt({
29017
+ client,
29018
+ auth,
29019
+ path: {
29020
+ id,
29021
+ n
29022
+ },
29023
+ body
29024
+ }));
29025
+ },
28781
29026
  async cancel(id, body) {
28782
29027
  return unwrapResult(await cancelTask({
28783
29028
  client,
@@ -28961,6 +29206,7 @@ function createAgent(options) {
28961
29206
  legreffier: createLegreffierNamespace(context),
28962
29207
  problems: createProblemsNamespace(context),
28963
29208
  teams: createTeamsNamespace(context),
29209
+ runtimeProfiles: createRuntimeProfilesNamespace(context),
28964
29210
  tasks: createTasksNamespace(context),
28965
29211
  client,
28966
29212
  getToken: () => tokenManager.getToken()
@@ -32268,6 +32514,63 @@ function pruneOldSnapshots(maxCached, currentDir) {
32268
32514
  });
32269
32515
  }
32270
32516
  //#endregion
32517
+ //#region ../../libs/pi-extension/src/abort-utils.ts
32518
+ function throwIfAborted(signal, label) {
32519
+ if (!signal?.aborted) return;
32520
+ throw abortError(label, signal);
32521
+ }
32522
+ function abortError(label, signal) {
32523
+ const reason = signal.reason;
32524
+ const suffix = reason instanceof Error ? reason.message : reason === void 0 ? "aborted" : String(reason);
32525
+ const err = /* @__PURE__ */ new Error(`${label} aborted: ${suffix}`);
32526
+ err.name = "AbortError";
32527
+ return err;
32528
+ }
32529
+ function cleanupLateResource(resourcePromise, opts) {
32530
+ resourcePromise.then(async (resource) => {
32531
+ try {
32532
+ await opts.cleanup(resource);
32533
+ } catch (err) {
32534
+ opts.onCleanupError?.(err);
32535
+ }
32536
+ }, () => {});
32537
+ }
32538
+ async function abortableResource(opts) {
32539
+ const { signal } = opts;
32540
+ if (!signal) return opts.promise;
32541
+ throwIfAborted(signal, opts.label);
32542
+ const resourcePromise = Promise.resolve(opts.promise);
32543
+ const abortPromise = new Promise((_, reject) => {
32544
+ const abort = () => {
32545
+ cleanupLateResource(resourcePromise, opts);
32546
+ reject(abortError(opts.label, signal));
32547
+ };
32548
+ signal.addEventListener("abort", abort, { once: true });
32549
+ resourcePromise.then(() => signal.removeEventListener("abort", abort), () => signal.removeEventListener("abort", abort));
32550
+ });
32551
+ return Promise.race([resourcePromise, abortPromise]);
32552
+ }
32553
+ async function delay(ms, signal, label) {
32554
+ if (!signal) {
32555
+ await new Promise((resolve) => {
32556
+ setTimeout(resolve, ms);
32557
+ });
32558
+ return;
32559
+ }
32560
+ throwIfAborted(signal, label);
32561
+ await new Promise((resolve, reject) => {
32562
+ const listener = () => {
32563
+ clearTimeout(timeout);
32564
+ reject(abortError(label, signal));
32565
+ };
32566
+ const timeout = setTimeout(() => {
32567
+ signal.removeEventListener("abort", listener);
32568
+ resolve();
32569
+ }, ms);
32570
+ signal.addEventListener("abort", listener, { once: true });
32571
+ });
32572
+ }
32573
+ //#endregion
32271
32574
  //#region ../../libs/pi-extension/src/vm-manager.ts
32272
32575
  /**
32273
32576
  * Memory-backed VFS mount used by the daemon to inject task-context
@@ -32384,23 +32687,33 @@ var BASE_ALLOWED_HOSTS = [
32384
32687
  * surface immediately rather than fall through to cryptic agent
32385
32688
  * errors later.
32386
32689
  */
32387
- async function vmRun(vm, label, command) {
32690
+ async function vmRun(vm, label, command, signal) {
32388
32691
  const wrapped = `set -eu\nset -o pipefail\n${command}`;
32692
+ throwIfAborted(signal, `resume step "${label}"`);
32389
32693
  const r = await vm.exec([
32390
32694
  "sh",
32391
32695
  "-c",
32392
32696
  wrapped
32393
- ]);
32697
+ ], { signal });
32394
32698
  if (r.exitCode !== 0) {
32395
32699
  const tail = [r.stderr, r.stdout].filter(Boolean).join("\n").slice(-800);
32396
32700
  throw new Error(`resume step "${label}" failed (exit ${r.exitCode}):\n${tail}`);
32397
32701
  }
32398
32702
  }
32703
+ function nonErrorMessage(err) {
32704
+ if (typeof err === "string") return err;
32705
+ try {
32706
+ return JSON.stringify(err) ?? "unknown error";
32707
+ } catch {
32708
+ return "unknown error";
32709
+ }
32710
+ }
32399
32711
  /**
32400
32712
  * Resume a VM from a checkpoint, inject credentials, configure egress +
32401
32713
  * TLS. Returns the managed VM handle.
32402
32714
  */
32403
32715
  async function resumeVm(config) {
32716
+ throwIfAborted(config.signal, "VM resume");
32404
32717
  const mainRepo = findMainWorktree();
32405
32718
  const agentDir = path.join(mainRepo, ".moltnet", config.agentName);
32406
32719
  const guestWorkspace = path.resolve(config.mountPath);
@@ -32444,24 +32757,33 @@ async function resumeVm(config) {
32444
32757
  };
32445
32758
  const resources = config.sandboxConfig?.resources;
32446
32759
  const workspaceMode = config.workspaceMode ?? "shared_mount";
32447
- const vm = await VmCheckpoint.load(config.checkpointPath).resume({
32448
- httpHooks,
32449
- env: vmEnv,
32450
- ...resources?.memory && { memory: resources.memory },
32451
- ...resources?.cpus && { cpus: resources.cpus },
32452
- vfs: { mounts: {
32453
- [guestWorkspace]: workspaceProvider,
32454
- [GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
32455
- } }
32760
+ const vm = await abortableResource({
32761
+ promise: VmCheckpoint.load(config.checkpointPath).resume({
32762
+ httpHooks,
32763
+ env: vmEnv,
32764
+ ...resources?.memory && { memory: resources.memory },
32765
+ ...resources?.cpus && { cpus: resources.cpus },
32766
+ vfs: { mounts: {
32767
+ [guestWorkspace]: workspaceProvider,
32768
+ [GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
32769
+ } }
32770
+ }),
32771
+ signal: config.signal,
32772
+ label: "VM resume",
32773
+ cleanup: (resumedVm) => resumedVm.close(),
32774
+ onCleanupError: (err) => {
32775
+ const message = err instanceof Error ? err.message : String(err);
32776
+ process.stderr.write(`[vm] aborted resume late vm.close() failed: ${message}\n`);
32777
+ }
32456
32778
  });
32457
32779
  try {
32458
- await vm.exec(`sh -c '
32780
+ await vmRun(vm, "TLS certificates", `
32459
32781
  cp /etc/gondolin/mitm/ca.crt /usr/local/share/ca-certificates/gondolin-mitm.crt
32460
32782
  update-ca-certificates 2>/dev/null
32461
32783
  cat /etc/gondolin/mitm/ca.crt >> /etc/ssl/certs/ca-certificates.crt
32462
- '`);
32463
- await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`);
32464
- await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`);
32784
+ `, config.signal);
32785
+ await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`, config.signal);
32786
+ await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`, config.signal);
32465
32787
  for (const [i, entry] of (config.sandboxConfig?.resumeCommands ?? []).entries()) {
32466
32788
  if (!shouldRunResumeCommand(entry, { workspaceMode })) continue;
32467
32789
  const { run, retries, backoffMs } = typeof entry === "string" ? {
@@ -32476,34 +32798,67 @@ async function resumeVm(config) {
32476
32798
  const label = `resumeCommands[${i}]`;
32477
32799
  let lastErr;
32478
32800
  for (let attempt = 0; attempt <= retries; attempt++) try {
32479
- await vmRun(vm, label, run);
32801
+ await vmRun(vm, label, run, config.signal);
32480
32802
  lastErr = void 0;
32481
32803
  break;
32482
32804
  } catch (err) {
32483
32805
  lastErr = err;
32484
32806
  if (attempt === retries) break;
32485
- await new Promise((resolve) => {
32486
- setTimeout(resolve, (attempt + 1) * backoffMs);
32487
- });
32807
+ await delay((attempt + 1) * backoffMs, config.signal, label);
32488
32808
  }
32489
- if (lastErr) throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
32809
+ if (lastErr) throw lastErr instanceof Error ? lastErr : new Error(nonErrorMessage(lastErr));
32490
32810
  }
32491
32811
  const vmSshDir = `${vmAgentDir}/ssh`;
32492
- await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`);
32493
- if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, { mode: 384 });
32812
+ await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`, { signal: config.signal });
32813
+ if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, {
32814
+ mode: 384,
32815
+ signal: config.signal
32816
+ });
32494
32817
  const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
32495
- await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, { mode: 384 });
32496
- await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, { mode: 384 });
32818
+ await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, {
32819
+ mode: 384,
32820
+ signal: config.signal
32821
+ });
32822
+ await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, {
32823
+ mode: 384,
32824
+ signal: config.signal
32825
+ });
32497
32826
  if (creds.gitconfig) {
32498
32827
  const vmSigningKey = `${vmSshDir}/id_ed25519`;
32499
32828
  const vmGitconfig = creds.gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSigningKey}`);
32500
- await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, { mode: 420 });
32829
+ await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, {
32830
+ mode: 420,
32831
+ signal: config.signal
32832
+ });
32501
32833
  }
32502
- if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, { mode: 384 });
32503
- if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, { mode: 420 });
32504
- if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, { mode: 420 });
32505
- if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, { mode: 384 });
32506
- await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet");
32834
+ if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, {
32835
+ mode: 384,
32836
+ signal: config.signal
32837
+ });
32838
+ if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, {
32839
+ mode: 420,
32840
+ signal: config.signal
32841
+ });
32842
+ if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, {
32843
+ mode: 420,
32844
+ signal: config.signal
32845
+ });
32846
+ if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, {
32847
+ mode: 384,
32848
+ signal: config.signal
32849
+ });
32850
+ await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet", { signal: config.signal });
32851
+ const gitCredHelperPath = `${vmSshDir}/git-credential-moltnet`;
32852
+ const credHelperScript = `#!/bin/sh
32853
+ echo "username=x-access-token"
32854
+ echo "password=$(moltnet github token --credentials ${vmSshDir}/moltnet.json)"
32855
+ `;
32856
+ await vm.fs.writeFile(gitCredHelperPath, credHelperScript, {
32857
+ mode: 493,
32858
+ signal: config.signal
32859
+ });
32860
+ await vmRun(vm, "git credential helper", `git config --global credential.helper ${gitCredHelperPath} && \
32861
+ git config --global url."https://github.com/".insteadOf "git@github.com:"`, config.signal);
32507
32862
  return {
32508
32863
  vm,
32509
32864
  credentials: creds,
@@ -33756,6 +34111,20 @@ async function executePiTask(claimedTask, reporter, opts) {
33756
34111
  retryable: false
33757
34112
  }
33758
34113
  });
34114
+ const makeCancelledOutput = (message) => ({
34115
+ taskId: task.id,
34116
+ attemptN,
34117
+ status: "cancelled",
34118
+ output: null,
34119
+ outputCid: null,
34120
+ usage: finalUsage,
34121
+ durationMs: Date.now() - startTime,
34122
+ error: {
34123
+ code: "task_cancelled",
34124
+ message,
34125
+ retryable: false
34126
+ }
34127
+ });
33759
34128
  let onTurnEvent;
33760
34129
  if (opts.makeOnTurnEvent) try {
33761
34130
  onTurnEvent = opts.makeOnTurnEvent(claimedTask);
@@ -33818,10 +34187,15 @@ async function executePiTask(claimedTask, reporter, opts) {
33818
34187
  mountPath,
33819
34188
  workspaceMode: workspace.mode,
33820
34189
  extraAllowedHosts: opts.extraAllowedHosts,
33821
- sandboxConfig
34190
+ sandboxConfig,
34191
+ signal: reporter.cancelSignal
33822
34192
  });
33823
34193
  } catch (err) {
33824
34194
  const message = err instanceof Error ? err.message : String(err);
34195
+ if (reporter.cancelSignal.aborted) {
34196
+ await emitError("vm_resume", message, { cancelled: true });
34197
+ return makeCancelledOutput(reporter.cancelReason ?? "Task cancelled during VM resume.");
34198
+ }
33825
34199
  await emitError("vm_resume", message);
33826
34200
  return makeFailedOutput("vm_resume_failed", message);
33827
34201
  }
@@ -34409,7 +34783,9 @@ function loadConfig() {
34409
34783
  return {
34410
34784
  agentDaemonStateDatabaseUrl: process.env["MOLTNET_AGENT_DAEMON_STATE_DATABASE_URL"] ?? "",
34411
34785
  otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
34412
- logLevel: process.env["LOG_LEVEL"] ?? ""
34786
+ logLevel: process.env["LOG_LEVEL"] ?? "",
34787
+ profilePrerequisiteEnv: process.env,
34788
+ profilePrerequisitePath: process.env.PATH ?? ""
34413
34789
  };
34414
34790
  }
34415
34791
  //#endregion
@@ -34837,13 +35213,27 @@ function createRootLogger(options) {
34837
35213
  target: "pino-pretty",
34838
35214
  options: { colorize: true }
34839
35215
  }) : null;
35216
+ let transportClosed = prettyTransport === null;
35217
+ let shutdownStarted = false;
35218
+ prettyTransport?.on("close", () => {
35219
+ transportClosed = true;
35220
+ });
35221
+ prettyTransport?.on("error", (err) => {
35222
+ transportClosed = true;
35223
+ process.stderr.write(`[pino] transport error: ` + (err instanceof Error ? err.message : String(err)) + "\n");
35224
+ });
34840
35225
  const logger = pino(options, prettyTransport ?? void 0);
34841
35226
  const shutdown = async () => {
34842
- if (!prettyTransport) return;
34843
- logger.flush();
35227
+ if (!prettyTransport || shutdownStarted || transportClosed) return;
35228
+ shutdownStarted = true;
35229
+ try {
35230
+ logger.flush();
35231
+ } catch (err) {
35232
+ process.stderr.write(`[pino] logger flush failed: ` + (err instanceof Error ? err.message : String(err)) + "\n");
35233
+ }
34844
35234
  try {
34845
35235
  prettyTransport.end();
34846
- await once(prettyTransport, "close");
35236
+ if (!transportClosed) await once(prettyTransport, "close");
34847
35237
  } catch (err) {
34848
35238
  process.stderr.write(`[pino] transport teardown failed: ` + (err instanceof Error ? err.message : String(err)) + "\n");
34849
35239
  }
@@ -34871,22 +35261,29 @@ var MissingRequiredOptionError = class extends Error {
34871
35261
  this.name = "MissingRequiredOptionError";
34872
35262
  }
34873
35263
  };
34874
- function parseCommonOptions(args) {
35264
+ function parseCommonOptions(args, options = {}) {
35265
+ const requireProviderModel = options.requireProviderModel ?? true;
35266
+ const runtimeDefaults = {
35267
+ leaseTtlSec: options.runtimeDefaults?.leaseTtlSec ?? DEFAULTS.leaseTtlSec,
35268
+ heartbeatIntervalMs: options.runtimeDefaults?.heartbeatIntervalMs ?? DEFAULTS.heartbeatIntervalMs,
35269
+ maxBatchSize: options.runtimeDefaults?.maxBatchSize ?? DEFAULTS.maxBatchSize,
35270
+ warmSessionTtlSec: options.runtimeDefaults?.warmSessionTtlSec ?? DEFAULTS.warmSessionTtlSec
35271
+ };
34875
35272
  if (!args.agent) throw new MissingRequiredOptionError("agent");
34876
- if (!args.provider) throw new MissingRequiredOptionError("provider");
34877
- if (!args.model) throw new MissingRequiredOptionError("model");
35273
+ if (requireProviderModel && !args.provider) throw new MissingRequiredOptionError("provider");
35274
+ if (requireProviderModel && !args.model) throw new MissingRequiredOptionError("model");
34878
35275
  if (!/^[a-zA-Z0-9_-]+$/.test(args.agent)) throw new Error(`Invalid --agent "${args.agent}": must match /^[a-zA-Z0-9_-]+$/`);
34879
35276
  return {
34880
35277
  agent: args.agent,
34881
- provider: args.provider,
34882
- model: args.model,
34883
- leaseTtlSec: parsePositiveInt(args["lease-ttl-sec"], "lease-ttl-sec", DEFAULTS.leaseTtlSec),
34884
- heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", DEFAULTS.heartbeatIntervalMs),
34885
- maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", DEFAULTS.maxBatchSize),
35278
+ ...args.provider ? { provider: args.provider } : {},
35279
+ ...args.model ? { model: args.model } : {},
35280
+ leaseTtlSec: parsePositiveInt(args["lease-ttl-sec"], "lease-ttl-sec", runtimeDefaults.leaseTtlSec),
35281
+ heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", runtimeDefaults.heartbeatIntervalMs),
35282
+ maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", runtimeDefaults.maxBatchSize),
34886
35283
  flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
34887
35284
  maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", DEFAULTS.maxTurns),
34888
35285
  maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", DEFAULTS.maxBashTimeouts),
34889
- warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", DEFAULTS.warmSessionTtlSec),
35286
+ warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", runtimeDefaults.warmSessionTtlSec),
34890
35287
  debug: args.debug === true
34891
35288
  };
34892
35289
  }
@@ -34977,6 +35374,71 @@ async function initWorkerOtel(options) {
34977
35374
  };
34978
35375
  }
34979
35376
  //#endregion
35377
+ //#region src/lib/runtime-profile.ts
35378
+ var RuntimeProfilePrerequisiteError = class extends Error {
35379
+ constructor(profileName, missingEnv, missingTools) {
35380
+ const parts = [missingEnv.length > 0 ? `missing env: ${missingEnv.join(", ")}` : null, missingTools.length > 0 ? `missing tools: ${missingTools.join(", ")}` : null].filter(Boolean);
35381
+ super(`Runtime profile "${profileName}" prerequisites are not satisfied: ${parts.join("; ")}`);
35382
+ this.profileName = profileName;
35383
+ this.missingEnv = missingEnv;
35384
+ this.missingTools = missingTools;
35385
+ this.name = "RuntimeProfilePrerequisiteError";
35386
+ }
35387
+ };
35388
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
35389
+ async function resolveRuntimeProfile(options) {
35390
+ const profile = UUID_RE.test(options.profile) ? await options.agent.runtimeProfiles.get(options.profile) : await resolveProfileByName(options);
35391
+ if (options.teamId && profile.teamId !== options.teamId) throw new Error(`Runtime profile "${options.profile}" belongs to team ${profile.teamId}, not ${options.teamId}.`);
35392
+ return {
35393
+ id: profile.id,
35394
+ name: profile.name,
35395
+ teamId: profile.teamId,
35396
+ provider: profile.provider,
35397
+ model: profile.model,
35398
+ leaseTtlSec: profile.leaseTtlSec,
35399
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
35400
+ maxBatchSize: profile.maxBatchSize,
35401
+ sessionTtlSec: profile.sessionTtlSec,
35402
+ workspaceTtlSec: profile.workspaceTtlSec,
35403
+ requiredEnv: profile.requiredEnv,
35404
+ requiredTools: profile.requiredTools,
35405
+ sandboxConfig: profile.sandbox,
35406
+ mountPath: resolve(options.cwd),
35407
+ source: `runtime-profile:${profile.id}`
35408
+ };
35409
+ }
35410
+ function validateRuntimeProfilePrerequisites(profile, env, pathValue) {
35411
+ const missingEnv = profile.requiredEnv.filter((name) => !env[name]);
35412
+ const missingTools = profile.requiredTools.filter((tool) => !isExecutableOnPath(tool, pathValue));
35413
+ if (missingEnv.length > 0 || missingTools.length > 0) throw new RuntimeProfilePrerequisiteError(profile.name, missingEnv, missingTools);
35414
+ }
35415
+ function resolveProfileWarmSessionTtlSec(profile) {
35416
+ return Math.min(profile.sessionTtlSec, profile.workspaceTtlSec);
35417
+ }
35418
+ async function resolveProfileByName(options) {
35419
+ if (!options.teamId) throw new Error(`Runtime profile name "${options.profile}" requires --team. Use a profile UUID when running without a team-scoped list.`);
35420
+ const matches = (await options.agent.runtimeProfiles.list({ teamId: options.teamId })).items.filter((item) => item.name === options.profile);
35421
+ if (matches.length === 0) throw new Error(`Runtime profile "${options.profile}" was not found in team ${options.teamId}.`);
35422
+ if (matches.length > 1) throw new Error(`Runtime profile name "${options.profile}" is ambiguous in team ${options.teamId}. Use the profile UUID instead.`);
35423
+ return matches[0];
35424
+ }
35425
+ function isExecutableOnPath(tool, pathValue) {
35426
+ if (tool.includes("/")) return isExecutable(isAbsolute(tool) ? tool : resolve(process.cwd(), tool));
35427
+ for (const dir of (pathValue ?? "").split(delimiter)) {
35428
+ if (!dir) continue;
35429
+ if (isExecutable(resolve(dir, tool))) return true;
35430
+ }
35431
+ return false;
35432
+ }
35433
+ function isExecutable(path) {
35434
+ try {
35435
+ accessSync(path, constants.X_OK);
35436
+ return true;
35437
+ } catch {
35438
+ return false;
35439
+ }
35440
+ }
35441
+ //#endregion
34980
35442
  //#region src/lib/sandbox.ts
34981
35443
  function resolveSandbox(startDir, explicitPath) {
34982
35444
  const path = explicitPath ? isAbsolute(explicitPath) ? explicitPath : resolve(startDir, explicitPath) : findUp(startDir, "sandbox.json");
@@ -35005,6 +35467,38 @@ function findUp(startDir, filename) {
35005
35467
  }
35006
35468
  }
35007
35469
  //#endregion
35470
+ //#region src/lib/shutdown-signal.ts
35471
+ function installShutdownSignalHandlers(opts) {
35472
+ const proc = opts.proc ?? process;
35473
+ let drainingSignal = null;
35474
+ const onSignal = (signal) => {
35475
+ if (drainingSignal) {
35476
+ proc.stderr.write(`[agent-daemon] ${signal} received while already draining from ${drainingSignal}; waiting for cleanup.\n`);
35477
+ proc.exitCode = signalExitCode(signal);
35478
+ return;
35479
+ }
35480
+ drainingSignal = signal;
35481
+ proc.exitCode = signalExitCode(signal);
35482
+ try {
35483
+ opts.logDrain(signal);
35484
+ } catch (err) {
35485
+ proc.stderr.write(`[agent-daemon] failed to log ${signal}: ` + (err instanceof Error ? err.message : String(err)) + "\n");
35486
+ }
35487
+ opts.drain(signal);
35488
+ };
35489
+ const handleSigint = () => onSignal("SIGINT");
35490
+ const handleSigterm = () => onSignal("SIGTERM");
35491
+ proc.on("SIGINT", handleSigint);
35492
+ proc.on("SIGTERM", handleSigterm);
35493
+ return { dispose: () => {
35494
+ proc.off("SIGINT", handleSigint);
35495
+ proc.off("SIGTERM", handleSigterm);
35496
+ } };
35497
+ }
35498
+ function signalExitCode(signal) {
35499
+ return signal === "SIGINT" ? 130 : 143;
35500
+ }
35501
+ //#endregion
35008
35502
  //#region src/lib/state-dir.ts
35009
35503
  function ensureDaemonStateDirs(mountPath) {
35010
35504
  const rootDir = join(mountPath, ".moltnet", "d");
@@ -35054,7 +35548,8 @@ async function runPolling(opts) {
35054
35548
  "poll-interval-ms": { type: "string" },
35055
35549
  "max-poll-interval-ms": { type: "string" },
35056
35550
  "list-limit": { type: "string" },
35057
- sandbox: { type: "string" }
35551
+ sandbox: { type: "string" },
35552
+ profile: { type: "string" }
35058
35553
  }
35059
35554
  });
35060
35555
  if (!values.team) {
@@ -35074,7 +35569,7 @@ async function runPolling(opts) {
35074
35569
  const diaryIds = parseCsv(values["diary-ids"]);
35075
35570
  let common;
35076
35571
  try {
35077
- common = parseCommonOptions(values);
35572
+ common = parseCommonOptions(values, { requireProviderModel: !values.profile });
35078
35573
  } catch (err) {
35079
35574
  if (err instanceof MissingRequiredOptionError) {
35080
35575
  console.error(`${err.message}\n`);
@@ -35086,15 +35581,45 @@ async function runPolling(opts) {
35086
35581
  const pollIntervalMs = optionalPositiveInt(values["poll-interval-ms"], "poll-interval-ms", 2e3);
35087
35582
  const maxPollIntervalMs = optionalPositiveInt(values["max-poll-interval-ms"], "max-poll-interval-ms", 3e4);
35088
35583
  const listLimit = optionalPositiveInt(values["list-limit"], "list-limit", 10);
35584
+ if (values.profile && values.sandbox) {
35585
+ console.error(`[${opts.modeLabel}] Cannot use --sandbox with --profile. Remote runtime profiles define sandbox policy.`);
35586
+ return 1;
35587
+ }
35089
35588
  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).`);
35090
- const sandbox = resolveSandbox(process.cwd(), values.sandbox);
35091
35589
  const cfg = loadConfig();
35590
+ const ctx = await resolveAgentContext(common.agent);
35591
+ const profile = values.profile ? await resolveRuntimeProfile({
35592
+ agent: ctx.agent,
35593
+ profile: values.profile,
35594
+ teamId,
35595
+ cwd: process.cwd()
35596
+ }) : null;
35597
+ if (profile) {
35598
+ validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
35599
+ common = parseCommonOptions(values, {
35600
+ requireProviderModel: false,
35601
+ runtimeDefaults: {
35602
+ leaseTtlSec: profile.leaseTtlSec,
35603
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
35604
+ maxBatchSize: profile.maxBatchSize,
35605
+ warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
35606
+ }
35607
+ });
35608
+ }
35609
+ const provider = profile?.provider ?? common.provider;
35610
+ const model = profile?.model ?? common.model;
35611
+ if (!provider || !model) throw new Error("provider/model missing after runtime profile resolution");
35612
+ const sandbox = profile ? {
35613
+ config: profile.sandboxConfig,
35614
+ rootDir: profile.mountPath,
35615
+ path: profile.source
35616
+ } : resolveSandbox(process.cwd(), values.sandbox);
35092
35617
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
35093
35618
  const slotRegistry = new DaemonSlotRegistry(resolveDaemonStateStorageConfig(stateDirs.registryDbPath, cfg.agentDaemonStateDatabaseUrl));
35094
35619
  const slotIdentity = {
35095
35620
  agentName: common.agent,
35096
- provider: common.provider,
35097
- model: common.model
35621
+ provider,
35622
+ model
35098
35623
  };
35099
35624
  const mainRepo = findMainWorktree();
35100
35625
  const executionPlans = createExecutionPlanCache({
@@ -35103,7 +35628,6 @@ async function runPolling(opts) {
35103
35628
  warmSessionTtlSec: common.warmSessionTtlSec,
35104
35629
  slotRegistry
35105
35630
  });
35106
- const ctx = await resolveAgentContext(common.agent);
35107
35631
  const otelShutdown = await initWorkerOtel({
35108
35632
  serviceName: opts.serviceName,
35109
35633
  agentDir: ctx.agentDir,
@@ -35111,8 +35635,9 @@ async function runPolling(opts) {
35111
35635
  resourceAttributes: {
35112
35636
  "moltnet.team.id": teamId,
35113
35637
  "moltnet.agent.name": common.agent,
35114
- "moltnet.llm.provider": common.provider,
35115
- "moltnet.llm.model": common.model
35638
+ "moltnet.llm.provider": provider,
35639
+ "moltnet.llm.model": model,
35640
+ ...profile ? { "moltnet.daemon_profile.id": profile.id } : {}
35116
35641
  }
35117
35642
  });
35118
35643
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -35123,46 +35648,82 @@ async function runPolling(opts) {
35123
35648
  mode: opts.modeLabel,
35124
35649
  agent: common.agent,
35125
35650
  teamId,
35126
- provider: common.provider,
35127
- model: common.model
35651
+ provider,
35652
+ model,
35653
+ ...profile ? {
35654
+ daemonProfileId: profile.id,
35655
+ daemonProfileName: profile.name
35656
+ } : {}
35128
35657
  });
35129
35658
  const abort = new AbortController();
35130
35659
  let runtime = null;
35131
- const onSignal = (sig) => {
35132
- rootLogger.warn({ signal: sig }, "agent-daemon.draining");
35133
- abort.abort();
35134
- runtime?.stop();
35135
- };
35136
- process.on("SIGINT", () => onSignal("SIGINT"));
35137
- process.on("SIGTERM", () => onSignal("SIGTERM"));
35660
+ let active = null;
35661
+ const signalHandlers = installShutdownSignalHandlers({
35662
+ logDrain: (signal) => {
35663
+ rootLogger.warn({ signal }, "agent-daemon.draining");
35664
+ },
35665
+ drain: (signal) => {
35666
+ abort.abort();
35667
+ runtime?.stop(`agent-daemon received ${signal}`);
35668
+ if (active === null) return;
35669
+ const { taskId, attemptN } = active;
35670
+ ctx.agent.tasks.abortAttempt(taskId, attemptN, { reason: `runner_${signal.toLowerCase()}` }).catch((err) => {
35671
+ try {
35672
+ rootLogger.warn({
35673
+ err: err instanceof Error ? err.message : String(err),
35674
+ taskId,
35675
+ attemptN
35676
+ }, "agent-daemon.abort_on_signal_failed");
35677
+ } catch {}
35678
+ });
35679
+ }
35680
+ });
35138
35681
  rootLogger.info({
35139
35682
  sandbox: sandbox.path,
35140
35683
  taskTypes: taskTypes.length > 0 ? taskTypes : ["*"],
35141
35684
  diaryIds: diaryIds.length > 0 ? diaryIds : ["*"],
35142
35685
  leaseTtlSec: common.leaseTtlSec,
35143
35686
  heartbeatIntervalMs: common.heartbeatIntervalMs,
35687
+ warmSessionTtlSec: common.warmSessionTtlSec,
35144
35688
  pollIntervalMs,
35145
- maxPollIntervalMs
35689
+ maxPollIntervalMs,
35690
+ ...profile ? {
35691
+ profileId: profile.id,
35692
+ profileSessionTtlSec: profile.sessionTtlSec,
35693
+ profileWorkspaceTtlSec: profile.workspaceTtlSec
35694
+ } : {}
35146
35695
  }, "agent-daemon.starting");
35147
35696
  const outputs = [];
35148
35697
  try {
35149
- const executeTask = createPiTaskExecutor({
35698
+ const rawExecuteTask = createPiTaskExecutor({
35150
35699
  agentName: common.agent,
35151
35700
  mountPath: sandbox.rootDir,
35152
- provider: common.provider,
35153
- model: common.model,
35701
+ provider,
35702
+ model,
35154
35703
  sandboxConfig: sandbox.config,
35155
35704
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
35156
35705
  makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger),
35157
35706
  maxTurns: common.maxTurns,
35158
35707
  maxBashTimeouts: common.maxBashTimeouts
35159
35708
  });
35709
+ const executeTask = async (claimedTask, reporter) => {
35710
+ active = {
35711
+ taskId: claimedTask.task.id,
35712
+ attemptN: claimedTask.attemptN
35713
+ };
35714
+ try {
35715
+ return await rawExecuteTask(claimedTask, reporter);
35716
+ } finally {
35717
+ active = null;
35718
+ }
35719
+ };
35160
35720
  runtime = new AgentRuntime({
35161
35721
  logger: rootLogger,
35162
35722
  source: new PollingApiTaskSource({
35163
35723
  agent: ctx.agent,
35164
35724
  teamId,
35165
35725
  taskTypes: taskTypes.length > 0 ? taskTypes : void 0,
35726
+ ...profile ? { profileId: profile.id } : {},
35166
35727
  diaryIds: diaryIds.length > 0 ? diaryIds : void 0,
35167
35728
  leaseTtlSec: common.leaseTtlSec,
35168
35729
  listLimit,
@@ -35309,6 +35870,7 @@ async function runPolling(opts) {
35309
35870
  rootLogger.info({ processed: drained.length }, "agent-daemon.drained");
35310
35871
  return drained.some((o) => o.status !== "completed") ? 1 : 0;
35311
35872
  } finally {
35873
+ signalHandlers.dispose();
35312
35874
  await slotRegistry.close();
35313
35875
  await otelShutdown();
35314
35876
  await shutdownLogger();
@@ -35353,7 +35915,8 @@ async function runOnce(argv) {
35353
35915
  type: "string",
35354
35916
  short: "t"
35355
35917
  },
35356
- sandbox: { type: "string" }
35918
+ sandbox: { type: "string" },
35919
+ profile: { type: "string" }
35357
35920
  }
35358
35921
  });
35359
35922
  if (!values["task-id"]) {
@@ -35364,7 +35927,7 @@ async function runOnce(argv) {
35364
35927
  const taskId = values["task-id"];
35365
35928
  let opts;
35366
35929
  try {
35367
- opts = parseCommonOptions(values);
35930
+ opts = parseCommonOptions(values, { requireProviderModel: !values.profile });
35368
35931
  } catch (err) {
35369
35932
  if (err instanceof MissingRequiredOptionError) {
35370
35933
  console.error(`${err.message}\n`);
@@ -35373,14 +35936,43 @@ async function runOnce(argv) {
35373
35936
  }
35374
35937
  throw err;
35375
35938
  }
35376
- const sandbox = resolveSandbox(process.cwd(), values.sandbox);
35939
+ if (values.profile && values.sandbox) {
35940
+ console.error("Cannot use --sandbox with --profile. Remote runtime profiles define sandbox policy.");
35941
+ return 1;
35942
+ }
35377
35943
  const cfg = loadConfig();
35944
+ const ctx = await resolveAgentContext(opts.agent);
35945
+ const profile = values.profile ? await resolveRuntimeProfile({
35946
+ agent: ctx.agent,
35947
+ profile: values.profile,
35948
+ cwd: process.cwd()
35949
+ }) : null;
35950
+ if (profile) {
35951
+ validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
35952
+ opts = parseCommonOptions(values, {
35953
+ requireProviderModel: false,
35954
+ runtimeDefaults: {
35955
+ leaseTtlSec: profile.leaseTtlSec,
35956
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
35957
+ maxBatchSize: profile.maxBatchSize,
35958
+ warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
35959
+ }
35960
+ });
35961
+ }
35962
+ const provider = profile?.provider ?? opts.provider;
35963
+ const model = profile?.model ?? opts.model;
35964
+ if (!provider || !model) throw new Error("provider/model missing after runtime profile resolution");
35965
+ const sandbox = profile ? {
35966
+ config: profile.sandboxConfig,
35967
+ rootDir: profile.mountPath,
35968
+ path: profile.source
35969
+ } : resolveSandbox(process.cwd(), values.sandbox);
35378
35970
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
35379
35971
  const slotRegistry = new DaemonSlotRegistry(resolveDaemonStateStorageConfig(stateDirs.registryDbPath, cfg.agentDaemonStateDatabaseUrl));
35380
35972
  const slotIdentity = {
35381
35973
  agentName: opts.agent,
35382
- provider: opts.provider,
35383
- model: opts.model
35974
+ provider,
35975
+ model
35384
35976
  };
35385
35977
  const mainRepo = findMainWorktree();
35386
35978
  const executionPlans = createExecutionPlanCache({
@@ -35389,7 +35981,6 @@ async function runOnce(argv) {
35389
35981
  warmSessionTtlSec: opts.warmSessionTtlSec,
35390
35982
  slotRegistry
35391
35983
  });
35392
- const ctx = await resolveAgentContext(opts.agent);
35393
35984
  const otelShutdown = await initWorkerOtel({
35394
35985
  serviceName: "moltnet.agent-daemon.once",
35395
35986
  agentDir: ctx.agentDir,
@@ -35397,8 +35988,9 @@ async function runOnce(argv) {
35397
35988
  resourceAttributes: {
35398
35989
  "moltnet.task.id": taskId,
35399
35990
  "moltnet.agent.name": opts.agent,
35400
- "moltnet.llm.provider": opts.provider,
35401
- "moltnet.llm.model": opts.model
35991
+ "moltnet.llm.provider": provider,
35992
+ "moltnet.llm.model": model,
35993
+ ...profile ? { "moltnet.daemon_profile.id": profile.id } : {}
35402
35994
  }
35403
35995
  });
35404
35996
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -35408,39 +36000,57 @@ async function runOnce(argv) {
35408
36000
  const rootLogger = logger.child({
35409
36001
  mode: "once",
35410
36002
  agent: opts.agent,
35411
- provider: opts.provider,
35412
- model: opts.model
36003
+ provider,
36004
+ model,
36005
+ ...profile ? {
36006
+ daemonProfileId: profile.id,
36007
+ daemonProfileName: profile.name
36008
+ } : {}
35413
36009
  });
35414
36010
  rootLogger.info({
35415
36011
  sandbox: sandbox.path,
35416
- taskId
36012
+ taskId,
36013
+ leaseTtlSec: opts.leaseTtlSec,
36014
+ heartbeatIntervalMs: opts.heartbeatIntervalMs,
36015
+ warmSessionTtlSec: opts.warmSessionTtlSec,
36016
+ ...profile ? {
36017
+ profileId: profile.id,
36018
+ profileSessionTtlSec: profile.sessionTtlSec,
36019
+ profileWorkspaceTtlSec: profile.workspaceTtlSec
36020
+ } : {}
35417
36021
  }, "agent-daemon.starting");
35418
36022
  let runtime = null;
35419
- const onSignal = (sig) => {
35420
- rootLogger.warn({
35421
- signal: sig,
35422
- taskId
35423
- }, "agent-daemon.draining");
35424
- runtime?.stop();
35425
- ctx.agent.tasks.cancel(taskId, { reason: `runner_${sig.toLowerCase()}` }).catch((err) => {
36023
+ let activeAttemptN = null;
36024
+ const signalHandlers = installShutdownSignalHandlers({
36025
+ logDrain: (signal) => {
35426
36026
  rootLogger.warn({
35427
- err: err instanceof Error ? err.message : String(err),
36027
+ signal,
35428
36028
  taskId
35429
- }, "agent-daemon.cancel_on_signal_failed");
35430
- });
35431
- };
35432
- process.on("SIGINT", () => {
35433
- onSignal("SIGINT");
35434
- });
35435
- process.on("SIGTERM", () => {
35436
- onSignal("SIGTERM");
36029
+ }, "agent-daemon.draining");
36030
+ },
36031
+ drain: (signal) => {
36032
+ runtime?.stop(`agent-daemon received ${signal}`);
36033
+ if (activeAttemptN === null) return;
36034
+ const attemptN = activeAttemptN;
36035
+ ctx.agent.tasks.abortAttempt(taskId, attemptN, { reason: `runner_${signal.toLowerCase()}` }).catch((err) => {
36036
+ try {
36037
+ rootLogger.warn({
36038
+ err: err instanceof Error ? err.message : String(err),
36039
+ taskId,
36040
+ attemptN
36041
+ }, "agent-daemon.abort_on_signal_failed");
36042
+ } catch (logErr) {
36043
+ process.stderr.write(`[agent-daemon] failed to log abort error: ` + (logErr instanceof Error ? logErr.message : String(logErr)) + "\n");
36044
+ }
36045
+ });
36046
+ }
35437
36047
  });
35438
36048
  try {
35439
36049
  const rawExecuteTask = createPiTaskExecutor({
35440
36050
  agentName: opts.agent,
35441
36051
  mountPath: sandbox.rootDir,
35442
- provider: opts.provider,
35443
- model: opts.model,
36052
+ provider,
36053
+ model,
35444
36054
  sandboxConfig: sandbox.config,
35445
36055
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
35446
36056
  onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
@@ -35501,9 +36111,11 @@ async function runOnce(argv) {
35501
36111
  lastAttemptN: claimedTask.attemptN,
35502
36112
  ttlSec: opts.warmSessionTtlSec
35503
36113
  });
36114
+ activeAttemptN = claimedTask.attemptN;
35504
36115
  try {
35505
36116
  return await rawExecuteTask(claimedTask, reporter);
35506
36117
  } finally {
36118
+ activeAttemptN = null;
35507
36119
  executionPlans.delete(claimedTask);
35508
36120
  if (executionPlan.slotKey) await slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, opts.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
35509
36121
  }
@@ -35517,7 +36129,8 @@ async function runOnce(argv) {
35517
36129
  source: new ApiTaskSource({
35518
36130
  agent: ctx.agent,
35519
36131
  taskId,
35520
- leaseTtlSec: opts.leaseTtlSec
36132
+ leaseTtlSec: opts.leaseTtlSec,
36133
+ ...profile ? { profileId: profile.id } : {}
35521
36134
  }),
35522
36135
  makeReporter: () => new ApiTaskReporter({
35523
36136
  tasks: ctx.agent.tasks,
@@ -35546,6 +36159,7 @@ async function runOnce(argv) {
35546
36159
  console.log(JSON.stringify(output, null, 2));
35547
36160
  return output.status === "completed" ? 0 : 1;
35548
36161
  } finally {
36162
+ signalHandlers.dispose();
35549
36163
  await slotRegistry.close();
35550
36164
  await otelShutdown();
35551
36165
  await shutdownLogger();