@themoltnet/pi-extension 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -163,6 +163,10 @@ the base snapshot is used (Alpine + git + gh + MoltNet CLI + agent user).
163
163
  { "argsPrefix": ["pr", "create"], "executable": "gh" }
164
164
  ]
165
165
  },
166
+ "network": {
167
+ "allowedHosts": ["api.example.com", "*.services.example.com"],
168
+ "allowedInternalHosts": ["onboard-api.internal"]
169
+ },
166
170
  "resources": {
167
171
  "cpus": 2,
168
172
  "memory": "6G"
@@ -201,6 +205,38 @@ Controls what's installed on top of the base layer during snapshot build.
201
205
  | `allowedHosts` | Extra hosts allowed during build (base hosts always included) |
202
206
  | `overlaySize` | qcow2 overlay disk size (default `"3G"`) |
203
207
 
208
+ ### `network`
209
+
210
+ Controls HTTP(S) egress while a VM is running. Both arrays accept exact
211
+ hostnames such as `api.example.com` and leading wildcard patterns such as
212
+ `*.example.com`. Do not include a URL scheme, port, or path.
213
+
214
+ - `allowedHosts` grants ordinary hostname egress. Gondolin still rejects a
215
+ matching hostname if DNS resolves it to loopback, link-local, or a private IP
216
+ range. This protects public allowlist entries from DNS rebinding and SSRF.
217
+ - `allowedInternalHosts` explicitly permits matching hostnames to resolve to
218
+ internal/private IP ranges. Gondolin automatically includes these entries in
219
+ its effective hostname allowlist, so they do not need to appear in both
220
+ arrays. Treat this as the stronger, security-sensitive permission.
221
+
222
+ The base runtime hosts, MoltNet API host, and legacy `extraAllowedHosts` remain
223
+ external-only. They are not implicitly allowed to resolve internally, and VM
224
+ resume rejects an `allowedInternalHosts` pattern that overlaps one of those
225
+ protected patterns. Use a distinct internal service hostname when private
226
+ resolution is required.
227
+
228
+ Runtime hosts are deliberately separate from `snapshot.allowedHosts`: build
229
+ dependencies do not become task-time egress grants, and runtime services do not
230
+ become snapshot build dependencies. Private destinations require an explicit
231
+ `allowedInternalHosts` grant; unlisted internal and external hosts remain
232
+ blocked.
233
+
234
+ Profiles and repo-local `sandbox.json` use the same field. For profiles, treat
235
+ this as a team-editable security boundary: forwarded environment values and
236
+ other VM-accessible secrets can be sent to any granted host. An internal grant
237
+ can additionally expose localhost services, cloud metadata, and private network
238
+ infrastructure if the hostname is attacker-controlled.
239
+
204
240
  ### `resources`
205
241
 
206
242
  VM resource limits applied at runtime.
package/dist/index.d.ts CHANGED
@@ -308,6 +308,12 @@ export declare interface ExecutePiTaskOptions {
308
308
  sandboxConfig?: SandboxConfig;
309
309
  /** Host environment variable names to forward into the Pi VM. */
310
310
  forwardEnv?: string[];
311
+ /**
312
+ * Runtime profile context defaults. Merged with task.input.context at
313
+ * execution time because the selected runtime profile is known only after
314
+ * claim. Task entries override profile entries with the same slug.
315
+ */
316
+ runtimeProfileContext?: readonly ContextRef[];
311
317
  /**
312
318
  * Forwarded to `buildTaskUserPrompt` for per-type builders. Static
313
319
  * across tasks. Today no built-in builder needs per-task `extras` —
@@ -480,14 +486,14 @@ export declare interface InjectedTaskContext {
480
486
  }
481
487
 
482
488
  /**
483
- * Resolve a task's `input.context[]` and inject the side effects pi
489
+ * Resolve effective runtime context and inject the side effects Pi
484
490
  * needs. Safe to call with an empty array — returns an inert result.
485
491
  */
486
492
  export declare function injectTaskContext(args: InjectTaskContextArgs): Promise<InjectedTaskContext>;
487
493
 
488
494
  export declare interface InjectTaskContextArgs {
489
495
  /** Empty array (the default for any non-eval task) is a no-op. */
490
- context: TaskContext;
496
+ context: readonly ContextRef[];
491
497
  /** Guest filesystem handle. In production this is `managed.vm.fs`. */
492
498
  fs: VmFsForContext;
493
499
  /** Guest path where the active host workspace is mounted. */
@@ -760,6 +766,14 @@ export declare interface SandboxConfig {
760
766
  /** Overlay disk size (default '3G'). */
761
767
  overlaySize?: string;
762
768
  };
769
+ /** Runtime network egress policy. Separate from snapshot build access. */
770
+ network?: {
771
+ /** Additional host patterns allowed while the VM is running.
772
+ * Internal and private address resolution remains blocked. */
773
+ allowedHosts?: string[];
774
+ /** Host patterns explicitly allowed to resolve to internal/private IPs. */
775
+ allowedInternalHosts?: string[];
776
+ };
763
777
  /** Shell commands to run every VM resume, after platform setup
764
778
  * (TLS, DNS, git safe.directory, tmpfs node_modules) and before
765
779
  * the agent session starts. Use for per-session bootstrap that
@@ -881,7 +895,7 @@ declare const Task: Type.TObject<{
881
895
  inputCid: Type.TString;
882
896
  references: Type.TArray<Type.TObject<{
883
897
  taskId: Type.TUnion<[Type.TString, Type.TNull]>;
884
- outputCid: Type.TString;
898
+ outputCid: Type.TOptional<Type.TString>;
885
899
  role: Type.TUnion<[Type.TLiteral<"judged_work">, Type.TLiteral<"reviewed_diff">, Type.TLiteral<"target_source">, Type.TLiteral<"context">]>;
886
900
  external: Type.TOptional<Type.TObject<{
887
901
  kind: Type.TUnion<[Type.TLiteral<"github_pr">, Type.TLiteral<"github_issue">, Type.TLiteral<"http_url">]>;
@@ -893,7 +907,7 @@ declare const Task: Type.TObject<{
893
907
  }>>;
894
908
  artifact: Type.TOptional<Type.TObject<{
895
909
  cid: Type.TString;
896
- attemptN: Type.TInteger;
910
+ attemptN: Type.TOptional<Type.TInteger>;
897
911
  kind: Type.TOptional<Type.TString>;
898
912
  title: Type.TOptional<Type.TString>;
899
913
  contentType: Type.TOptional<Type.TString>;
@@ -922,15 +936,6 @@ declare const Task: Type.TObject<{
922
936
 
923
937
  declare type Task = Static<typeof Task>;
924
938
 
925
- /** Reusable input fragment for any task type. Soft cap at 5 items. */
926
- declare const TaskContext: Type.TArray<Type.TObject<{
927
- slug: Type.TString;
928
- binding: Type.TUnion<[Type.TLiteral<"skill">, Type.TLiteral<"context_inline">, Type.TLiteral<"prompt_prefix">, Type.TLiteral<"user_inline">]>;
929
- content: Type.TString;
930
- }>>;
931
-
932
- declare type TaskContext = Static<typeof TaskContext>;
933
-
934
939
  declare const TaskMessage: Type.TObject<{
935
940
  taskId: Type.TString;
936
941
  attemptN: Type.TNumber;
package/dist/index.js CHANGED
@@ -1965,7 +1965,34 @@ var findLatestRuntimeSlotForAttempt = (options) => (options.client ?? client).ge
1965
1965
  ...options
1966
1966
  });
1967
1967
  /**
1968
- * Queue asynchronous deletion of terminal tasks in bulk. By default, live, unauthorized, missing, and protected tasks are skipped. Set force: true with a reason to delete protected terminal tasks.
1968
+ * Stage immutable content-addressed artifact bytes for later binding as task input artifacts via task creation references. Creates no metadata row; staged bytes are not downloadable until bound to a task, and unbound objects are garbage-collected after a grace window.
1969
+ */
1970
+ var stageTaskArtifact = (options) => (options.client ?? client).put({
1971
+ bodySerializer: null,
1972
+ security: [
1973
+ {
1974
+ scheme: "bearer",
1975
+ type: "http"
1976
+ },
1977
+ {
1978
+ name: "X-Moltnet-Session-Token",
1979
+ type: "apiKey"
1980
+ },
1981
+ {
1982
+ in: "cookie",
1983
+ name: "ory_kratos_session",
1984
+ type: "apiKey"
1985
+ }
1986
+ ],
1987
+ url: "/task-artifacts/staged",
1988
+ ...options,
1989
+ headers: {
1990
+ "Content-Type": "application/octet-stream",
1991
+ ...options.headers
1992
+ }
1993
+ });
1994
+ /**
1995
+ * Queue asynchronous deletion of waiting, queued, and terminal tasks in bulk. By default, dispatched, running, unauthorized, missing, and protected tasks are skipped. Set force: true with a reason to delete protected terminal tasks.
1969
1996
  */
1970
1997
  var batchDeleteTasks = (options) => (options.client ?? client).delete({
1971
1998
  security: [
@@ -9671,6 +9698,11 @@ var RuntimeProfileSandboxResumeCommand = Union([String$1({
9671
9698
  maximum: 6e4
9672
9699
  }))
9673
9700
  }, { additionalProperties: false })]);
9701
+ var RuntimeProfileAllowedHost = String$1({
9702
+ minLength: 1,
9703
+ maxLength: 255,
9704
+ pattern: "^(?:\\*\\.)?(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\\.(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*$"
9705
+ });
9674
9706
  var RuntimeProfileSandbox = _Object_({
9675
9707
  snapshot: Optional(_Object_({
9676
9708
  setupCommands: Optional(_Array_(String$1({
@@ -9687,6 +9719,10 @@ var RuntimeProfileSandbox = _Object_({
9687
9719
  pattern: "^[0-9]+[KMGTP]?$"
9688
9720
  }))
9689
9721
  }, { additionalProperties: false })),
9722
+ network: Optional(_Object_({
9723
+ allowedHosts: Optional(_Array_(RuntimeProfileAllowedHost, { maxItems: 50 })),
9724
+ allowedInternalHosts: Optional(_Array_(RuntimeProfileAllowedHost, { maxItems: 50 }))
9725
+ }, { additionalProperties: false })),
9690
9726
  resumeCommands: Optional(_Array_(RuntimeProfileSandboxResumeCommand, { maxItems: 30 })),
9691
9727
  vfs: Optional(_Object_({
9692
9728
  shadow: Optional(_Array_(String$1({
@@ -10030,7 +10066,7 @@ _Object_({
10030
10066
  * (server-side schema check). Self-assessment is a truthful self-rating,
10031
10067
  * NOT enforcement — `verification.passed=false` does not block /complete
10032
10068
  * and does not affect `acceptedAttemptN`. See
10033
- * `docs/understand/agent-runtime.md` for the full producer/judge flow.
10069
+ * `docs/use/tasks-and-runtime.md` for the full producer/judge flow.
10034
10070
  *
10035
10071
  * **Binding evaluation** (judgment tasks: `assess_brief`, `judge_pack`).
10036
10072
  * A separate task whose IS the application of `successCriteria` to
@@ -10146,7 +10182,7 @@ _Object_({
10146
10182
  id: String$1({ format: "uuid" }),
10147
10183
  teamId: String$1({ format: "uuid" }),
10148
10184
  taskId: String$1({ format: "uuid" }),
10149
- attemptN: Integer({ minimum: 1 }),
10185
+ attemptN: Union([Integer({ minimum: 1 }), Null()]),
10150
10186
  kind: String$1({
10151
10187
  minLength: 1,
10152
10188
  maxLength: 100
@@ -10168,7 +10204,7 @@ _Object_({
10168
10204
  minLength: 1,
10169
10205
  maxLength: 100
10170
10206
  }),
10171
- createdByAgentId: String$1({ format: "uuid" }),
10207
+ createdByAgentId: Union([String$1({ format: "uuid" }), Null()]),
10172
10208
  expiresAt: Union([String$1({ format: "date-time" }), Null()]),
10173
10209
  createdAt: String$1({ format: "date-time" })
10174
10210
  }, { $id: "TaskArtifact" })),
@@ -10184,6 +10220,16 @@ _Object_({
10184
10220
  $id: "ListTaskArtifactsQuery",
10185
10221
  additionalProperties: false
10186
10222
  });
10223
+ var HeaderSafeContentType = String$1({
10224
+ minLength: 1,
10225
+ maxLength: 200,
10226
+ pattern: "^[\\x21-\\x7e][\\x20-\\x7e]*$"
10227
+ });
10228
+ var HeaderSafeContentEncoding = String$1({
10229
+ minLength: 1,
10230
+ maxLength: 100,
10231
+ pattern: "^[\\x21-\\x7e][\\x20-\\x7e]*$"
10232
+ });
10187
10233
  _Object_({
10188
10234
  kind: String$1({
10189
10235
  minLength: 1,
@@ -10193,14 +10239,8 @@ _Object_({
10193
10239
  minLength: 1,
10194
10240
  maxLength: 255
10195
10241
  }),
10196
- contentType: Optional(String$1({
10197
- minLength: 1,
10198
- maxLength: 200
10199
- })),
10200
- contentEncoding: Optional(String$1({
10201
- minLength: 1,
10202
- maxLength: 100
10203
- }))
10242
+ contentType: Optional(HeaderSafeContentType),
10243
+ contentEncoding: Optional(HeaderSafeContentEncoding)
10204
10244
  }, {
10205
10245
  $id: "UploadTaskArtifactQuery",
10206
10246
  additionalProperties: false
@@ -10232,6 +10272,34 @@ _Object_({
10232
10272
  $id: "TaskArtifactContentParams",
10233
10273
  additionalProperties: false
10234
10274
  });
10275
+ _Object_({
10276
+ contentType: Optional(HeaderSafeContentType),
10277
+ contentEncoding: Optional(HeaderSafeContentEncoding)
10278
+ }, {
10279
+ $id: "StageTaskArtifactQuery",
10280
+ additionalProperties: false
10281
+ });
10282
+ _Object_({
10283
+ cid: String$1({
10284
+ minLength: 1,
10285
+ maxLength: 100
10286
+ }),
10287
+ sizeBytes: Integer({ minimum: 0 }),
10288
+ contentType: String$1({
10289
+ minLength: 1,
10290
+ maxLength: 200
10291
+ })
10292
+ }, { $id: "StagedTaskArtifact" });
10293
+ _Object_({
10294
+ taskId: String$1({ format: "uuid" }),
10295
+ cid: String$1({
10296
+ minLength: 1,
10297
+ maxLength: 100
10298
+ })
10299
+ }, {
10300
+ $id: "TaskArtifactTaskContentParams",
10301
+ additionalProperties: false
10302
+ });
10235
10303
  //#endregion
10236
10304
  //#region ../../node_modules/.pnpm/multiformats@13.4.2/node_modules/multiformats/dist/src/codecs/json.js
10237
10305
  var textEncoder$2 = new TextEncoder();
@@ -13847,6 +13915,42 @@ function validateTaskCreateRequest(args) {
13847
13915
  field: "references",
13848
13916
  message: `At least one reference is required for task type: ${args.taskType}`
13849
13917
  });
13918
+ errors.push(...validateTaskReferences(args.references));
13919
+ return errors;
13920
+ }
13921
+ /**
13922
+ * Cross-field rules for the reference shapes the schema alone cannot
13923
+ * express. Every reference must be exactly one of:
13924
+ *
13925
+ * - **task output ref**: taskId set, outputCid required; an optional
13926
+ * artifact must name its producing attempt (attemptN).
13927
+ * - **input artifact ref**: taskId null, artifact without attemptN;
13928
+ * outputCid omitted (or equal to artifact.cid when sent) -- the bytes
13929
+ * were staged before any task existed, so there is no output to name.
13930
+ * - **external ref**: taskId null, external present, no artifact.
13931
+ *
13932
+ * Anything else used to be silently persisted into taskRefs and never
13933
+ * materialized; now it fails fast with a field error.
13934
+ */
13935
+ function validateTaskReferences(references) {
13936
+ const errors = [];
13937
+ (references ?? []).forEach((ref, index) => {
13938
+ const invalid = (message) => errors.push({
13939
+ field: `references[${index}]`,
13940
+ message
13941
+ });
13942
+ if (ref.taskId !== null) {
13943
+ if (ref.outputCid === void 0) invalid("outputCid is required when referencing a task output");
13944
+ if (ref.artifact && ref.artifact.attemptN === void 0) invalid("artifact references to another task must include attemptN; input artifacts are referenced with taskId null");
13945
+ return;
13946
+ }
13947
+ if (ref.artifact) {
13948
+ if (ref.artifact.attemptN !== void 0) invalid("input artifact references (taskId null) must not include attemptN; reference the producing task by id instead");
13949
+ if (ref.outputCid !== void 0 && ref.outputCid !== ref.artifact.cid) invalid("outputCid on an input artifact reference must be omitted or equal artifact.cid");
13950
+ return;
13951
+ }
13952
+ if (!ref.external) invalid("references with taskId null must carry either an artifact (input artifact) or an external descriptor");
13953
+ });
13850
13954
  return errors;
13851
13955
  }
13852
13956
  //#endregion
@@ -13963,7 +14067,7 @@ Unsafe(Cyclic({ ClaimCondition: Unsafe(Union([
13963
14067
  */
13964
14068
  var TaskRef = _Object_({
13965
14069
  taskId: Union([Uuid, Null()]),
13966
- outputCid: Cid,
14070
+ outputCid: Optional(Cid),
13967
14071
  role: Union([
13968
14072
  Literal("judged_work"),
13969
14073
  Literal("reviewed_diff"),
@@ -13984,7 +14088,7 @@ var TaskRef = _Object_({
13984
14088
  })),
13985
14089
  artifact: Optional(_Object_({
13986
14090
  cid: Cid,
13987
- attemptN: Integer({ minimum: 1 }),
14091
+ attemptN: Optional(Integer({ minimum: 1 })),
13988
14092
  kind: Optional(String$1({
13989
14093
  minLength: 1,
13990
14094
  maxLength: 100
@@ -14454,30 +14558,53 @@ var TaskBuilder = class {
14454
14558
  return this;
14455
14559
  }
14456
14560
  /**
14457
- * Add a reference to a persistent task artifact while retaining the accepted
14458
- * output CID as the provenance anchor.
14561
+ * Add either a staged input artifact or a persistent attempt artifact.
14562
+ * Staged metadata returned by `tasks.artifacts.stage()` carries
14563
+ * `artifactSource: 'staged'` and produces a reference with `taskId: null`, no
14564
+ * `outputCid`, and no `attemptN`. Persistent artifacts require the producing
14565
+ * task, accepted output CID, artifact CID, and positive attempt number so
14566
+ * their provenance remains explicit.
14459
14567
  *
14460
- * @param source - A result reader, raw artifact reference, or `TaskRef`.
14568
+ * @param source - Staged SDK metadata, a result reader, raw artifact reference, or `TaskRef`.
14461
14569
  * @param role - The role the referenced artifact plays.
14462
14570
  * @returns This builder, for chaining.
14463
- * @throws {TaskBuildError} when output or artifact CID is missing.
14571
+ * @throws {TaskBuildError} when the source is ambiguous or required provenance is missing.
14464
14572
  */
14465
14573
  artifactReference(source, role) {
14466
14574
  let ref;
14467
14575
  if ("artifactRef" in source && typeof source.artifactRef === "function") ref = source.artifactRef(role);
14468
- else if ("artifact" in source && source.artifact?.cid) {
14469
- if (typeof source.artifact.attemptN !== "number" || !Number.isInteger(source.artifact.attemptN) || source.artifact.attemptN < 1) throw new TaskBuildError([{
14470
- field: "references/artifact/attemptN",
14471
- message: "artifact reference is missing required attemptN"
14472
- }]);
14473
- ref = {
14474
- ...source,
14475
- role
14476
- };
14477
- } else {
14576
+ else if ("artifactSource" in source && source.artifactSource === "staged" && source.cid) ref = {
14577
+ taskId: null,
14578
+ role,
14579
+ artifact: {
14580
+ cid: source.cid,
14581
+ ...source.kind ? { kind: source.kind } : {},
14582
+ ...source.title ? { title: source.title } : {},
14583
+ ...source.contentType ? { contentType: source.contentType } : {}
14584
+ }
14585
+ };
14586
+ else if ("cid" in source) throw new TaskBuildError([{
14587
+ field: "references/artifactSource",
14588
+ message: "top-level artifact CID is ambiguous; use metadata returned by tasks.artifacts.stage()"
14589
+ }]);
14590
+ else if ("artifact" in source && source.artifact?.cid) if (source.taskId === null && source.artifact.attemptN === void 0) ref = {
14591
+ taskId: null,
14592
+ role,
14593
+ artifact: { ...source.artifact }
14594
+ };
14595
+ else if (typeof source.artifact.attemptN !== "number" || !Number.isInteger(source.artifact.attemptN) || source.artifact.attemptN < 1) throw new TaskBuildError([{
14596
+ field: "references/artifact/attemptN",
14597
+ message: "artifact reference is missing required attemptN"
14598
+ }]);
14599
+ else ref = {
14600
+ ...source,
14601
+ role
14602
+ };
14603
+ else {
14478
14604
  const s = source;
14479
14605
  const errors = [];
14480
- if (!s.outputCid) errors.push({
14606
+ const inputArtifact = s.taskId === null && s.attemptN === void 0;
14607
+ if (!inputArtifact && !s.outputCid) errors.push({
14481
14608
  field: "references/outputCid",
14482
14609
  message: "reference is missing required outputCid"
14483
14610
  });
@@ -14485,19 +14612,18 @@ var TaskBuilder = class {
14485
14612
  field: "references/artifact/cid",
14486
14613
  message: "artifact reference is missing required cid"
14487
14614
  });
14488
- if (typeof s.attemptN !== "number" || !Number.isInteger(s.attemptN) || s.attemptN < 1) errors.push({
14615
+ if (!inputArtifact && (typeof s.attemptN !== "number" || !Number.isInteger(s.attemptN) || s.attemptN < 1)) errors.push({
14489
14616
  field: "references/artifact/attemptN",
14490
14617
  message: "artifact reference is missing required attemptN"
14491
14618
  });
14492
14619
  if (errors.length > 0) throw new TaskBuildError(errors);
14493
- const attemptN = s.attemptN;
14494
14620
  ref = {
14495
14621
  taskId: s.taskId ?? null,
14496
- outputCid: s.outputCid,
14622
+ ...!inputArtifact && s.outputCid ? { outputCid: s.outputCid } : {},
14497
14623
  role,
14498
14624
  artifact: {
14499
14625
  cid: s.artifactCid,
14500
- attemptN,
14626
+ ...s.attemptN !== void 0 ? { attemptN: s.attemptN } : {},
14501
14627
  ...s.kind ? { kind: s.kind } : {},
14502
14628
  ...s.title ? { title: s.title } : {},
14503
14629
  ...s.contentType ? { contentType: s.contentType } : {}
@@ -14967,6 +15093,22 @@ function createTasksNamespace(context) {
14967
15093
  }));
14968
15094
  },
14969
15095
  artifacts: {
15096
+ async stage(body, query, options) {
15097
+ return {
15098
+ ...unwrapResult(await stageTaskArtifact({
15099
+ auth,
15100
+ body,
15101
+ client,
15102
+ duplex: "half",
15103
+ headers: {
15104
+ ...requiredTeamHeaders(options),
15105
+ "content-type": "application/octet-stream"
15106
+ },
15107
+ query
15108
+ })),
15109
+ artifactSource: "staged"
15110
+ };
15111
+ },
14970
15112
  async upload(path, body, query, options) {
14971
15113
  return unwrapResult(await uploadTaskArtifact({
14972
15114
  auth,
@@ -15000,17 +15142,24 @@ function createTasksNamespace(context) {
15000
15142
  }));
15001
15143
  },
15002
15144
  async download(path, options) {
15003
- const result = await client.request({
15145
+ const request = {
15004
15146
  auth,
15005
15147
  headers: requiredTeamHeaders(options),
15006
15148
  method: "GET",
15007
15149
  parseAs: "stream",
15008
- path,
15009
15150
  security: [{
15010
15151
  scheme: "bearer",
15011
15152
  type: "http"
15012
- }],
15153
+ }]
15154
+ };
15155
+ const result = "attemptN" in path ? await client.request({
15156
+ ...request,
15157
+ path,
15013
15158
  url: "/tasks/{taskId}/attempts/{attemptN}/artifacts/{cid}/content"
15159
+ }) : await client.request({
15160
+ ...request,
15161
+ path,
15162
+ url: "/tasks/{taskId}/artifacts/{cid}/content"
15014
15163
  });
15015
15164
  const normalizedStream = normalizeDownloadStream(unwrapResult(result));
15016
15165
  if (normalizedStream) return {
@@ -19010,6 +19159,39 @@ var BASE_ALLOWED_HOSTS = [
19010
19159
  "*.googlesource.com"
19011
19160
  ];
19012
19161
  /**
19162
+ * Return whether two Gondolin hostname globs can match at least one common
19163
+ * string. Each `*` is an arbitrary substring, so this walks the product of the
19164
+ * two small glob automata instead of relying on exact-string comparisons.
19165
+ */
19166
+ function hostnamePatternsOverlap(left, right) {
19167
+ const a = left.trim().toLowerCase();
19168
+ const b = right.trim().toLowerCase();
19169
+ if (!a || !b) return false;
19170
+ const pending = [[0, 0]];
19171
+ const visited = /* @__PURE__ */ new Set();
19172
+ while (pending.length > 0) {
19173
+ const next = pending.pop();
19174
+ if (!next) continue;
19175
+ const [aIndex, bIndex] = next;
19176
+ const state = `${aIndex}:${bIndex}`;
19177
+ if (visited.has(state)) continue;
19178
+ visited.add(state);
19179
+ if (aIndex === a.length && bIndex === b.length) return true;
19180
+ const aChar = a[aIndex];
19181
+ const bChar = b[bIndex];
19182
+ if (aChar === "*") pending.push([aIndex + 1, bIndex]);
19183
+ if (bChar === "*") pending.push([aIndex, bIndex + 1]);
19184
+ if (aChar !== void 0 && bChar !== void 0 && (aChar === "*" || bChar === "*" || aChar === bChar)) pending.push([aChar === "*" ? aIndex : aIndex + 1, bChar === "*" ? bIndex : bIndex + 1]);
19185
+ }
19186
+ return false;
19187
+ }
19188
+ function assertInternalHostsDoNotOverlapProtectedHosts(internalHosts, protectedHosts) {
19189
+ for (const internalHost of internalHosts) {
19190
+ const protectedHost = protectedHosts.find((candidate) => hostnamePatternsOverlap(internalHost, candidate));
19191
+ if (protectedHost) throw new Error(`sandbox.network.allowedInternalHosts pattern "${internalHost}" overlaps external-only host pattern "${protectedHost}"`);
19192
+ }
19193
+ }
19194
+ /**
19013
19195
  * Run a shell command in the guest and throw if it fails. Mirror of
19014
19196
  * `run()` in `snapshot.ts` for the resume-side hook chain — every
19015
19197
  * setup step is essential to a healthy session, so a silent non-zero
@@ -19051,11 +19233,18 @@ async function resumeVm(config) {
19051
19233
  const creds = loadCredentials(agentDir);
19052
19234
  const moltnetConfig = JSON.parse(creds.moltnetJson);
19053
19235
  const apiHost = new URL(moltnetConfig.endpoints.api).hostname;
19054
- const { httpHooks, env: secretEnv } = createHttpHooks({ allowedHosts: [
19236
+ const runtimeAllowedHosts = config.sandboxConfig?.network?.allowedHosts ?? [];
19237
+ const runtimeAllowedInternalHosts = config.sandboxConfig?.network?.allowedInternalHosts ?? [];
19238
+ const protectedExternalHosts = [...new Set([
19055
19239
  ...BASE_ALLOWED_HOSTS,
19056
19240
  apiHost,
19057
19241
  ...config.extraAllowedHosts ?? []
19058
- ] });
19242
+ ])];
19243
+ assertInternalHostsDoNotOverlapProtectedHosts(runtimeAllowedInternalHosts, protectedExternalHosts);
19244
+ const { httpHooks, env: secretEnv } = createHttpHooks({
19245
+ allowedHosts: [...new Set([...protectedExternalHosts, ...runtimeAllowedHosts])],
19246
+ allowedInternalHosts: runtimeAllowedInternalHosts
19247
+ });
19059
19248
  const vmAgentDir = `/home/agent/.moltnet/${config.agentName}`;
19060
19249
  const vmAgentEnv = {};
19061
19250
  for (const [k, v] of Object.entries(creds.agentEnv)) {
@@ -19937,8 +20126,19 @@ async function resolvePersistentSessionManager(args) {
19937
20126
  //#endregion
19938
20127
  //#region ../agent-runtime/src/context-bindings.ts
19939
20128
  var PROMPT_SEPARATOR = "\n\n---\n\n";
20129
+ var MAX_MERGED_RUNTIME_CONTEXT_ENTRIES = 10;
20130
+ /**
20131
+ * Merge runtime-profile context defaults with task-scoped context. Profile
20132
+ * entries are defaults; task entries with the same slug override them.
20133
+ */
20134
+ function mergeRuntimeProfileContext(profileContext, taskContext) {
20135
+ const taskSlugs = new Set(taskContext.map((ref) => ref.slug));
20136
+ const merged = [...profileContext.filter((ref) => !taskSlugs.has(ref.slug)), ...taskContext];
20137
+ if (merged.length > MAX_MERGED_RUNTIME_CONTEXT_ENTRIES) throw new Error(`merged runtime context has ${merged.length} entries; maximum is ${MAX_MERGED_RUNTIME_CONTEXT_ENTRIES}`);
20138
+ return merged;
20139
+ }
19940
20140
  /**
19941
- * Resolve `task.input.context[]` into delivered side-effects (skills
20141
+ * Resolve runtime context entries into delivered side-effects (skills
19942
20142
  * persisted via `deliver.skill`) and prompt fragments
19943
20143
  * (`systemPromptPrefix`, `userInlineSuffix`) the caller weaves into the
19944
20144
  * built prompt.
@@ -19958,9 +20158,10 @@ var PROMPT_SEPARATOR = "\n\n---\n\n";
19958
20158
  * - `user_inline` → content appended to `userInlineSuffix` in
19959
20159
  * declared order, same separator.
19960
20160
  *
19961
- * No fetching, no hashing — bytes are inlined in `ContextRef.content`,
19962
- * and the task's `inputCid` already pins the entire input. The proposer
19963
- * chose these bytes; the resolver just dispatches them.
20161
+ * No fetching, no hashing — bytes are inlined in `ContextRef.content`.
20162
+ * Task-scoped entries are pinned by the task's `inputCid`; profile-scoped
20163
+ * entries are pinned by the runtime profile revision/source the daemon
20164
+ * resolved. The resolver just dispatches already-selected bytes.
19964
20165
  *
19965
20166
  * The function is pure with respect to its arguments: file writes are
19966
20167
  * confined to the injected `deliver` callback, which makes the
@@ -20006,12 +20207,13 @@ function formatInlineContextBlock(slug, content) {
20006
20207
  "### Injected Task Context",
20007
20208
  "",
20008
20209
  `Context id: \`${slug}\``,
20009
- "The following raw context was supplied by the task creator. Treat it",
20010
- "as task-relevant background that may override generic coding instincts",
20011
- "when it contains repo- or workflow-specific constraints.",
20210
+ "The following raw context was selected for this task by its task input",
20211
+ "or runtime profile. Treat it as task-relevant background that may",
20212
+ "override generic coding instincts when it contains repo- or",
20213
+ "workflow-specific constraints.",
20012
20214
  "The same content may also be materialized by the runtime under",
20013
20215
  "`/moltnet-task-context/context` for tool-based inspection. Do not",
20014
- "create or rely on workspace mirror files for this task context.",
20216
+ "create or rely on workspace mirror files for this runtime context.",
20015
20217
  "",
20016
20218
  "<context>",
20017
20219
  content,
@@ -21334,11 +21536,11 @@ function buildRenderPackUserPrompt(input, ctx) {
21334
21536
  * `judge_eval_attempt` task(s) grade against their own hidden rubric.
21335
21537
  *
21336
21538
  * Context delivery is handled by `resolveTaskContext` (see
21337
- * libs/agent-runtime/src/context-bindings.ts) and runs BEFORE this
21338
- * prompt is rendered: `prompt_prefix` items are concatenated ahead of
21339
- * the body, `skill` items are persisted at the runtime's skill path,
21340
- * and `user_inline` items are appended to the first user message. This
21341
- * builder does NOT inline `input.context[]` itself.
21539
+ * libs/agent-runtime/src/context-bindings.ts) and is selected BEFORE this
21540
+ * prompt is rendered. Task-scoped context lives in `input.context`; runtime
21541
+ * profile defaults arrive as `ctx.effectiveRuntimeContext` after the runtime
21542
+ * merges them with task context. This builder only renders context
21543
+ * discipline; it does NOT inline context bytes itself.
21342
21544
  *
21343
21545
  * Prompt-shape notes (issue #1175, area 1):
21344
21546
  * - No `Correlation` section: the agent never acts on it. The id is
@@ -21356,12 +21558,13 @@ function buildRenderPackUserPrompt(input, ctx) {
21356
21558
  */
21357
21559
  function buildRunEvalUserPrompt(input, ctx) {
21358
21560
  const { scenario, variantLabel, successCriteria } = input;
21359
- const hasContext = input.context.length > 0;
21360
- const hasInlineContext = input.context.some((entry) => entry.binding === "context_inline");
21561
+ const effectiveRuntimeContext = ctx.effectiveRuntimeContext ?? input.context;
21562
+ const hasContext = effectiveRuntimeContext.length > 0;
21563
+ const hasInlineContext = effectiveRuntimeContext.some((entry) => entry.binding === "context_inline");
21361
21564
  const header = `# Run Eval Agent\n\nYou are running an evaluation scenario as variant \`${variantLabel}\`.\nTask id: \`${ctx.taskId}\``;
21362
21565
  const contextDiscipline = hasContext ? [
21363
21566
  "This task includes Injected Task Context supplied by the task",
21364
- "creator. You MUST inspect it BEFORE you write solution files or",
21567
+ "input or runtime profile. You MUST inspect it BEFORE you write solution files or",
21365
21568
  "draft your final answer — not after.",
21366
21569
  "",
21367
21570
  "Reconcile every constraint from that context **into the code path",
@@ -21534,7 +21737,8 @@ function buildTaskUserPrompt(task, ctx) {
21534
21737
  return buildRunEvalUserPrompt(task.input, {
21535
21738
  diaryId: ctx.diaryId,
21536
21739
  taskId: ctx.taskId,
21537
- correlationId: task.correlationId
21740
+ correlationId: task.correlationId,
21741
+ effectiveRuntimeContext: ctx.effectiveRuntimeContext
21538
21742
  });
21539
21743
  default: throw new Error(`No prompt builder registered for taskType="${task.taskType}"`);
21540
21744
  }
@@ -25007,119 +25211,6 @@ var require_multistream = /* @__PURE__ */ __commonJSMin(((exports, module) => {
25007
25211
  module.exports.pino = pino;
25008
25212
  })))();
25009
25213
  //#endregion
25010
- //#region src/runtime/inject-task-context.ts
25011
- /**
25012
- * Slice 1.5 of #943 — wire the agent-runtime resolver into the
25013
- * pi-extension execution path.
25014
- *
25015
- * `resolveTaskContext` is a pure dispatcher; this module provides the
25016
- * Gondolin-aware deliverer and the post-resolution shape the
25017
- * `execute-pi-task` caller needs to splice into pi's setup:
25018
- *
25019
- * - `systemPromptPrefix` → fed into `appendSystemPrompt` alongside
25020
- * the runtime instructor (it IS a system-prompt fragment).
25021
- * - `userInlineSuffix` → appended to the `buildTaskUserPrompt`
25022
- * output BEFORE `session.prompt(text)`.
25023
- * - `skills` → spliced into the `skillsOverride` callback's
25024
- * return value. pi includes them in `<available_skills>` in the
25025
- * system prompt; the agent fetches the body on demand via the
25026
- * Read tool.
25027
- *
25028
- * Task-context files are written into a memory-backed VM mount. pi only reads
25029
- * `<available_skills>` metadata (name, description, location), never the skill
25030
- * body, so we construct synthetic `Skill` objects pointing at the in-VM path
25031
- * without ever materialising the file on the host.
25032
- */
25033
- /**
25034
- * Where in the VM we write task-context bodies — the memory-backed mount
25035
- * declared in `vm-manager.ts`. See the comment on
25036
- * `GUEST_TASK_CONTEXT_MOUNT` there for the full rationale (ephemeral by
25037
- * intent + the worktree symlink interaction with Gondolin's sandbox-escape
25038
- * protection). The agent's Gondolin Read tool accepts paths under this mount
25039
- * via `toGuestPath` in `tool-operations.ts`.
25040
- */
25041
- var SKILL_ROOT_IN_VM = `${GUEST_TASK_CONTEXT_MOUNT}/skills`;
25042
- var INLINE_CONTEXT_ROOT_IN_VM = `${GUEST_TASK_CONTEXT_MOUNT}/context`;
25043
- /** Bounds borrowed from pi's skill validation; conservative caps so a
25044
- * malformed SKILL.md doesn't bloat the system prompt. */
25045
- var MAX_SKILL_NAME = 64;
25046
- var MAX_SKILL_DESCRIPTION = 1024;
25047
- /**
25048
- * Resolve a task's `input.context[]` and inject the side effects pi
25049
- * needs. Safe to call with an empty array — returns an inert result.
25050
- */
25051
- async function injectTaskContext(args) {
25052
- const skills = [];
25053
- args.guestWorkspace;
25054
- const resolved = await resolveTaskContext({
25055
- context: args.context,
25056
- deliver: {
25057
- skill: async ({ slug, content }) => {
25058
- const dir = `${SKILL_ROOT_IN_VM}/${slug}`;
25059
- const filePath = `${dir}/SKILL.md`;
25060
- await args.fs.mkdir(dir, { recursive: true });
25061
- await args.fs.writeFile(filePath, content, { mode: 420 });
25062
- skills.push(buildSyntheticSkill({
25063
- slug,
25064
- content,
25065
- filePath,
25066
- dir
25067
- }));
25068
- },
25069
- contextFile: async ({ suggestedFileName, content }) => {
25070
- await args.fs.mkdir(INLINE_CONTEXT_ROOT_IN_VM, { recursive: true });
25071
- const filePath = `${INLINE_CONTEXT_ROOT_IN_VM}/${suggestedFileName}`;
25072
- await args.fs.writeFile(filePath, content, { mode: 420 });
25073
- }
25074
- }
25075
- });
25076
- return {
25077
- injected: resolved.injected,
25078
- skills,
25079
- systemPromptPrefix: resolved.systemPromptPrefix,
25080
- userInlineSuffix: resolved.userInlineSuffix
25081
- };
25082
- }
25083
- /**
25084
- * Build a `Skill` object pi will faithfully render in
25085
- * `<available_skills>`. We extract `name` and `description` from the
25086
- * skill content's YAML frontmatter using pi's own `parseFrontmatter`
25087
- * helper (proper YAML, not a regex hack) and fall back to the slug +
25088
- * a generic description so a SKILL.md without frontmatter still
25089
- * renders something meaningful.
25090
- *
25091
- * Frontmatter parsing is best-effort: a malformed YAML block is
25092
- * optional metadata, not a reason to fail the task. We swallow parser
25093
- * errors and fall back to the slug-derived metadata; the skill body
25094
- * is unaffected.
25095
- *
25096
- * pi's `formatSkillsForPrompt` only reads `name`, `description`, and
25097
- * `filePath` — `sourceInfo`/`baseDir` exist on the type but never
25098
- * surface in the prompt, so a synthetic `SourceInfo` is enough.
25099
- */
25100
- function buildSyntheticSkill(args) {
25101
- let fm = {};
25102
- try {
25103
- fm = parseFrontmatter(args.content).frontmatter;
25104
- } catch {}
25105
- return {
25106
- name: clip(typeof fm.name === "string" && fm.name.trim().length > 0 ? fm.name.trim() : args.slug, MAX_SKILL_NAME),
25107
- description: clip(typeof fm.description === "string" && fm.description.trim().length > 0 ? fm.description.trim() : `Task-injected context skill (${args.slug})`, MAX_SKILL_DESCRIPTION),
25108
- filePath: args.filePath,
25109
- baseDir: args.dir,
25110
- sourceInfo: createSyntheticSourceInfo(args.filePath, {
25111
- source: "moltnet:task-context",
25112
- scope: "temporary",
25113
- origin: "top-level",
25114
- baseDir: args.dir
25115
- }),
25116
- disableModelInvocation: fm["disable-model-invocation"] === true
25117
- };
25118
- }
25119
- function clip(s, max) {
25120
- return s.length > max ? s.slice(0, max) : s;
25121
- }
25122
- //#endregion
25123
25214
  //#region src/runtime/resolve-prior-context.ts
25124
25215
  /**
25125
25216
  * Fetch the named attempt's output and project it into the prompt's
@@ -25306,6 +25397,113 @@ async function withTimeout(promise, timeoutMs, onTimeout) {
25306
25397
  }
25307
25398
  }
25308
25399
  //#endregion
25400
+ //#region src/runtime/runtime-context.ts
25401
+ /**
25402
+ * Pi-specific runtime context handling.
25403
+ *
25404
+ * `@themoltnet/agent-runtime` owns generic context semantics: merge profile
25405
+ * defaults with task context, resolve bindings, and produce prompt fragments.
25406
+ * This module owns the Pi/Gondolin boundary: validate effective context for an
25407
+ * attempt, write skill/context files into the VM, and build synthetic Pi Skill
25408
+ * metadata for injected skill bindings.
25409
+ */
25410
+ /**
25411
+ * Where in the VM we write runtime-context bodies — the memory-backed mount
25412
+ * declared in `vm-manager.ts`. See the comment on
25413
+ * `GUEST_TASK_CONTEXT_MOUNT` there for the full rationale (ephemeral by
25414
+ * intent + the worktree symlink interaction with Gondolin's sandbox-escape
25415
+ * protection). The agent's Gondolin Read tool accepts paths under this mount
25416
+ * via `toGuestPath` in `tool-operations.ts`.
25417
+ */
25418
+ var SKILL_ROOT_IN_VM = `${GUEST_TASK_CONTEXT_MOUNT}/skills`;
25419
+ var INLINE_CONTEXT_ROOT_IN_VM = `${GUEST_TASK_CONTEXT_MOUNT}/context`;
25420
+ /** Bounds borrowed from pi's skill validation; conservative caps so a
25421
+ * malformed SKILL.md doesn't bloat the system prompt. */
25422
+ var MAX_SKILL_NAME = 64;
25423
+ var MAX_SKILL_DESCRIPTION = 1024;
25424
+ function resolveEffectiveRuntimeContext(args) {
25425
+ const taskContext = args.rawTaskContext === void 0 ? [] : args.rawTaskContext;
25426
+ if (!Check(TaskContext, taskContext)) throw new Error(`task.input.context failed TaskContext validation: ${JSON.stringify([...Errors(TaskContext, taskContext)].slice(0, 3))}`);
25427
+ const profileContext = args.runtimeProfileContext ?? [];
25428
+ if (!Check(TaskContext, profileContext)) throw new Error(`runtime profile context failed TaskContext validation: ${JSON.stringify([...Errors(TaskContext, profileContext)].slice(0, 3))}`);
25429
+ return mergeRuntimeProfileContext(profileContext, taskContext);
25430
+ }
25431
+ /**
25432
+ * Resolve effective runtime context and inject the side effects Pi
25433
+ * needs. Safe to call with an empty array — returns an inert result.
25434
+ */
25435
+ async function injectRuntimeContext(args) {
25436
+ const skills = [];
25437
+ args.guestWorkspace;
25438
+ const resolved = await resolveTaskContext({
25439
+ context: args.context,
25440
+ deliver: {
25441
+ skill: async ({ slug, content }) => {
25442
+ const dir = `${SKILL_ROOT_IN_VM}/${slug}`;
25443
+ const filePath = `${dir}/SKILL.md`;
25444
+ await args.fs.mkdir(dir, { recursive: true });
25445
+ await args.fs.writeFile(filePath, content, { mode: 420 });
25446
+ skills.push(buildSyntheticSkill({
25447
+ slug,
25448
+ content,
25449
+ filePath,
25450
+ dir
25451
+ }));
25452
+ },
25453
+ contextFile: async ({ suggestedFileName, content }) => {
25454
+ await args.fs.mkdir(INLINE_CONTEXT_ROOT_IN_VM, { recursive: true });
25455
+ const filePath = `${INLINE_CONTEXT_ROOT_IN_VM}/${suggestedFileName}`;
25456
+ await args.fs.writeFile(filePath, content, { mode: 420 });
25457
+ }
25458
+ }
25459
+ });
25460
+ return {
25461
+ injected: resolved.injected,
25462
+ skills,
25463
+ systemPromptPrefix: resolved.systemPromptPrefix,
25464
+ userInlineSuffix: resolved.userInlineSuffix
25465
+ };
25466
+ }
25467
+ /**
25468
+ * Build a `Skill` object pi will faithfully render in
25469
+ * `<available_skills>`. We extract `name` and `description` from the
25470
+ * skill content's YAML frontmatter using pi's own `parseFrontmatter`
25471
+ * helper (proper YAML, not a regex hack) and fall back to the slug +
25472
+ * a generic description so a SKILL.md without frontmatter still
25473
+ * renders something meaningful.
25474
+ *
25475
+ * Frontmatter parsing is best-effort: a malformed YAML block is
25476
+ * optional metadata, not a reason to fail the task. We swallow parser
25477
+ * errors and fall back to the slug-derived metadata; the skill body
25478
+ * is unaffected.
25479
+ *
25480
+ * pi's `formatSkillsForPrompt` only reads `name`, `description`, and
25481
+ * `filePath` — `sourceInfo`/`baseDir` exist on the type but never
25482
+ * surface in the prompt, so a synthetic `SourceInfo` is enough.
25483
+ */
25484
+ function buildSyntheticSkill(args) {
25485
+ let fm = {};
25486
+ try {
25487
+ fm = parseFrontmatter(args.content).frontmatter;
25488
+ } catch {}
25489
+ return {
25490
+ name: clip(typeof fm.name === "string" && fm.name.trim().length > 0 ? fm.name.trim() : args.slug, MAX_SKILL_NAME),
25491
+ description: clip(typeof fm.description === "string" && fm.description.trim().length > 0 ? fm.description.trim() : `Runtime-injected context skill (${args.slug})`, MAX_SKILL_DESCRIPTION),
25492
+ filePath: args.filePath,
25493
+ baseDir: args.dir,
25494
+ sourceInfo: createSyntheticSourceInfo(args.filePath, {
25495
+ source: "moltnet:runtime-context",
25496
+ scope: "temporary",
25497
+ origin: "top-level",
25498
+ baseDir: args.dir
25499
+ }),
25500
+ disableModelInvocation: fm["disable-model-invocation"] === true
25501
+ };
25502
+ }
25503
+ function clip(s, max) {
25504
+ return s.length > max ? s.slice(0, max) : s;
25505
+ }
25506
+ //#endregion
25309
25507
  //#region src/runtime/subagent-tool.ts
25310
25508
  var SUBAGENT_SUBMIT_TOOL_NAME = "submit_subagent_output";
25311
25509
  var DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES = 2;
@@ -25841,6 +26039,46 @@ function resolveSubmitTools(taskType, opts = {}) {
25841
26039
  };
25842
26040
  }
25843
26041
  //#endregion
26042
+ //#region src/runtime/task-event-emitter.ts
26043
+ var LOG_TRUNCATE_LIMIT = 4 * 1024;
26044
+ async function emitTaskEvent(input) {
26045
+ try {
26046
+ input.onTurnEvent(input.kind, summarizePayloadForLog(input.kind, input.payload));
26047
+ } catch (err) {
26048
+ process.stderr.write(`[emit] onTurnEvent threw for kind="${input.kind}": ${err instanceof Error ? err.message : String(err)}\n`);
26049
+ }
26050
+ try {
26051
+ await input.reporter.record({
26052
+ kind: input.kind,
26053
+ payload: input.payload
26054
+ });
26055
+ } catch (err) {
26056
+ const detail = err instanceof Error ? err.message : String(err);
26057
+ input.log(`executePiTask: reporter.record() failed for task ${input.taskId} attempt ${input.attemptN} kind="${input.kind}": ${detail}`);
26058
+ }
26059
+ }
26060
+ function summarizePayloadForLog(kind, payload) {
26061
+ switch (kind) {
26062
+ case "text_delta": {
26063
+ const delta = payload.delta;
26064
+ return { chars: typeof delta === "string" ? delta.length : 0 };
26065
+ }
26066
+ case "tool_call_start": return { tool: payload.tool_name };
26067
+ case "tool_call_end": return {
26068
+ tool: payload.tool_name,
26069
+ is_error: payload.is_error === true,
26070
+ ...payload.is_error === true && payload.result !== void 0 ? { result: payload.result } : {}
26071
+ };
26072
+ case "turn_end": return { stop_reason: payload.stop_reason };
26073
+ case "error": return {
26074
+ phase: payload.phase,
26075
+ message: typeof payload.message === "string" ? payload.message.slice(0, LOG_TRUNCATE_LIMIT) : payload.message
26076
+ };
26077
+ case "info": return Object.fromEntries(Object.entries(payload).map(([k, v]) => [k, typeof v === "string" ? v.slice(0, LOG_TRUNCATE_LIMIT) : v]));
26078
+ default: return payload;
26079
+ }
26080
+ }
26081
+ //#endregion
25844
26082
  //#region src/runtime/task-workspace.ts
25845
26083
  function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
25846
26084
  const branch = executionPlan?.worktreeBranch ?? null;
@@ -26209,15 +26447,17 @@ async function executePiTask(claimedTask, reporter, opts) {
26209
26447
  onTurnEvent = noopTurnEventHandler;
26210
26448
  }
26211
26449
  else onTurnEvent = opts.onTurnEvent ?? noopTurnEventHandler;
26212
- const emit = (kind, payload) => {
26213
- try {
26214
- onTurnEvent(kind, summarizePayloadForLog(kind, payload));
26215
- } catch (err) {
26216
- process.stderr.write(`[emit] onTurnEvent threw for kind="${kind}": ${err instanceof Error ? err.message : String(err)}\n`);
26217
- }
26218
- return reporter.record({
26450
+ const emit = async (kind, payload) => {
26451
+ await emitTaskEvent({
26219
26452
  kind,
26220
- payload
26453
+ payload,
26454
+ onTurnEvent,
26455
+ reporter,
26456
+ taskId: task.id,
26457
+ attemptN,
26458
+ log: (message) => {
26459
+ process.stderr.write(`${message}\n`);
26460
+ }
26221
26461
  });
26222
26462
  };
26223
26463
  const emitError = async (phase, message, extra = {}) => {
@@ -26321,6 +26561,21 @@ async function executePiTask(claimedTask, reporter, opts) {
26321
26561
  message
26322
26562
  });
26323
26563
  }
26564
+ const rawContext = task.input.context;
26565
+ let effectiveRuntimeContext;
26566
+ try {
26567
+ effectiveRuntimeContext = resolveEffectiveRuntimeContext({
26568
+ rawTaskContext: rawContext,
26569
+ runtimeProfileContext: opts.runtimeProfileContext
26570
+ });
26571
+ } catch (err) {
26572
+ const message = err instanceof Error ? err.message : String(err);
26573
+ await emit("error", {
26574
+ message,
26575
+ phase: "context_resolution"
26576
+ });
26577
+ return makeFailedOutput("context_resolution_failed", message);
26578
+ }
26324
26579
  let taskPrompt;
26325
26580
  try {
26326
26581
  const assembled = buildTaskUserPrompt(task, {
@@ -26333,7 +26588,8 @@ async function executePiTask(claimedTask, reporter, opts) {
26333
26588
  source: executionPlan?.workspaceSeed?.source === "producer" ? "producer_copy" : executionPlan?.workspaceAttachment !== void 0 ? "producer_attachment" : void 0
26334
26589
  },
26335
26590
  extras: opts.promptExtras,
26336
- priorContext: resolvedPriorContext
26591
+ priorContext: resolvedPriorContext,
26592
+ effectiveRuntimeContext
26337
26593
  });
26338
26594
  taskPrompt = assembled.text;
26339
26595
  await emit("info", {
@@ -26350,13 +26606,10 @@ async function executePiTask(claimedTask, reporter, opts) {
26350
26606
  });
26351
26607
  return makeFailedOutput("prompt_build_failed", message);
26352
26608
  }
26353
- const rawContext = task.input.context;
26354
26609
  let injectedContext;
26355
26610
  try {
26356
- const contextArray = rawContext === void 0 ? [] : rawContext;
26357
- if (!Check(TaskContext, contextArray)) throw new Error(`task.input.context failed TaskContext validation: ${JSON.stringify([...Errors(TaskContext, contextArray)].slice(0, 3))}`);
26358
- injectedContext = await injectTaskContext({
26359
- context: contextArray,
26611
+ injectedContext = await injectRuntimeContext({
26612
+ context: effectiveRuntimeContext,
26360
26613
  fs: managed.vm.fs,
26361
26614
  guestWorkspace: managed.guestWorkspace
26362
26615
  });
@@ -26948,27 +27201,6 @@ function wireSessionAbort(cancelSignal, session) {
26948
27201
  * `task_messages.payload` row. Bodies above 4 KiB are replaced with a
26949
27202
  * `{ truncated, original_size }` marker so the JSONL/DB size stays bounded.
26950
27203
  */
26951
- function summarizePayloadForLog(kind, payload) {
26952
- switch (kind) {
26953
- case "text_delta": {
26954
- const delta = payload.delta;
26955
- return { chars: typeof delta === "string" ? delta.length : 0 };
26956
- }
26957
- case "tool_call_start": return { tool: payload.tool_name };
26958
- case "tool_call_end": return {
26959
- tool: payload.tool_name,
26960
- is_error: payload.is_error === true,
26961
- ...payload.is_error === true && payload.result !== void 0 ? { result: payload.result } : {}
26962
- };
26963
- case "turn_end": return { stop_reason: payload.stop_reason };
26964
- case "error": return {
26965
- phase: payload.phase,
26966
- message: typeof payload.message === "string" ? payload.message.slice(0, TRUNCATE_LIMIT) : payload.message
26967
- };
26968
- case "info": return Object.fromEntries(Object.entries(payload).map(([k, v]) => [k, typeof v === "string" ? v.slice(0, TRUNCATE_LIMIT) : v]));
26969
- default: return payload;
26970
- }
26971
- }
26972
27204
  /**
26973
27205
  * Classify a `tool_execution_end` event for telemetry purposes.
26974
27206
  *
@@ -27565,4 +27797,4 @@ function moltnetExtension(pi) {
27565
27797
  registerMoltnetReflectCommand(pi, state);
27566
27798
  }
27567
27799
  //#endregion
27568
- export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiProviderErrorRetryUi, createPiRetryTriage, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveTaskWorktreePath, resumeVm, toGuestPath };
27800
+ export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiProviderErrorRetryUi, createPiRetryTriage, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectRuntimeContext as injectTaskContext, loadCredentials, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveTaskWorktreePath, resumeVm, toGuestPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "type": "module",
5
5
  "description": "MoltNet pi extension — sandboxed tool execution in Gondolin VMs with MoltNet identity and persistent memory",
6
6
  "keywords": [
@@ -36,8 +36,8 @@
36
36
  "@earendil-works/gondolin": "^0.9.1",
37
37
  "@opentelemetry/api": "^1.9.0",
38
38
  "typebox": "^1.2.8",
39
- "@themoltnet/agent-runtime": "0.35.0",
40
- "@themoltnet/sdk": "0.119.0"
39
+ "@themoltnet/agent-runtime": "0.35.2",
40
+ "@themoltnet/sdk": "0.120.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@earendil-works/pi-coding-agent": ">=0.74.0",