@themoltnet/pi-extension 0.33.1 → 0.34.1

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
@@ -766,6 +766,14 @@ export declare interface SandboxConfig {
766
766
  /** Overlay disk size (default '3G'). */
767
767
  overlaySize?: string;
768
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
+ };
769
777
  /** Shell commands to run every VM resume, after platform setup
770
778
  * (TLS, DNS, git safe.directory, tmpfs node_modules) and before
771
779
  * the agent session starts. Use for per-session bootstrap that
@@ -887,7 +895,7 @@ declare const Task: Type.TObject<{
887
895
  inputCid: Type.TString;
888
896
  references: Type.TArray<Type.TObject<{
889
897
  taskId: Type.TUnion<[Type.TString, Type.TNull]>;
890
- outputCid: Type.TString;
898
+ outputCid: Type.TOptional<Type.TString>;
891
899
  role: Type.TUnion<[Type.TLiteral<"judged_work">, Type.TLiteral<"reviewed_diff">, Type.TLiteral<"target_source">, Type.TLiteral<"context">]>;
892
900
  external: Type.TOptional<Type.TObject<{
893
901
  kind: Type.TUnion<[Type.TLiteral<"github_pr">, Type.TLiteral<"github_issue">, Type.TLiteral<"http_url">]>;
@@ -899,7 +907,7 @@ declare const Task: Type.TObject<{
899
907
  }>>;
900
908
  artifact: Type.TOptional<Type.TObject<{
901
909
  cid: Type.TString;
902
- attemptN: Type.TInteger;
910
+ attemptN: Type.TOptional<Type.TInteger>;
903
911
  kind: Type.TOptional<Type.TString>;
904
912
  title: Type.TOptional<Type.TString>;
905
913
  contentType: Type.TOptional<Type.TString>;
package/dist/index.js CHANGED
@@ -1965,6 +1965,33 @@ var findLatestRuntimeSlotForAttempt = (options) => (options.client ?? client).ge
1965
1965
  ...options
1966
1966
  });
1967
1967
  /**
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
+ /**
1968
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({
@@ -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({
@@ -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 {
@@ -18294,13 +18443,13 @@ function createMoltNetTools(config) {
18294
18443
  const downloadTaskArtifact = defineTool({
18295
18444
  name: "moltnet_download_task_artifact",
18296
18445
  label: "Download MoltNet Task Artifact",
18297
- description: "Download immutable task artifact bytes by taskId, attemptN, and CID into a new file in the current task workspace. Use moltnet_list_task_artifacts first to choose the correct CID for referenced task inputs.",
18446
+ description: "Download immutable task artifact bytes by taskId and CID into a new file in the current task workspace. Use moltnet_list_task_artifacts first to choose the correct CID. Omit attemptN for a bound input artifact; pass it only to require an artifact from one exact task attempt.",
18298
18447
  parameters: Type.Object({
18299
18448
  taskId: Type.Optional(Type.String({ description: "Task ID. Defaults to the active task when running inside a task attempt." })),
18300
- attemptN: Type.Integer({
18449
+ attemptN: Type.Optional(Type.Integer({
18301
18450
  minimum: 1,
18302
- description: "Attempt number that produced the artifact."
18303
- }),
18451
+ description: "Attempt number that produced the artifact. Omit for bound input artifacts, which have no producing attempt."
18452
+ })),
18304
18453
  cid: Type.String({
18305
18454
  minLength: 1,
18306
18455
  description: "Artifact CID returned by moltnet_list_task_artifacts."
@@ -18314,11 +18463,15 @@ function createMoltNetTools(config) {
18314
18463
  if (!taskId) throw new Error("moltnet_download_task_artifact requires taskId outside an active task");
18315
18464
  const cwd = config.getHostCwd?.() ?? process.cwd();
18316
18465
  const outputPath = await resolveWorkspaceOutputPath(cwd, params.outputPath);
18317
- const download = await agent.tasks.artifacts.download({
18466
+ const artifactPath = params.attemptN === void 0 ? {
18467
+ taskId,
18468
+ cid: params.cid
18469
+ } : {
18318
18470
  taskId,
18319
18471
  attemptN: params.attemptN,
18320
18472
  cid: params.cid
18321
- }, { teamId });
18473
+ };
18474
+ const download = await agent.tasks.artifacts.download(artifactPath, { teamId });
18322
18475
  await pipeline(download.stream, createWriteStream(outputPath, { flags: "wx" }));
18323
18476
  const info = await stat(outputPath);
18324
18477
  return {
@@ -18326,7 +18479,7 @@ function createMoltNetTools(config) {
18326
18479
  type: "text",
18327
18480
  text: JSON.stringify({
18328
18481
  taskId,
18329
- attemptN: params.attemptN,
18482
+ ...params.attemptN === void 0 ? {} : { attemptN: params.attemptN },
18330
18483
  cid: params.cid,
18331
18484
  artifactId: download.artifactId,
18332
18485
  contentType: download.contentType,
@@ -19010,6 +19163,39 @@ var BASE_ALLOWED_HOSTS = [
19010
19163
  "*.googlesource.com"
19011
19164
  ];
19012
19165
  /**
19166
+ * Return whether two Gondolin hostname globs can match at least one common
19167
+ * string. Each `*` is an arbitrary substring, so this walks the product of the
19168
+ * two small glob automata instead of relying on exact-string comparisons.
19169
+ */
19170
+ function hostnamePatternsOverlap(left, right) {
19171
+ const a = left.trim().toLowerCase();
19172
+ const b = right.trim().toLowerCase();
19173
+ if (!a || !b) return false;
19174
+ const pending = [[0, 0]];
19175
+ const visited = /* @__PURE__ */ new Set();
19176
+ while (pending.length > 0) {
19177
+ const next = pending.pop();
19178
+ if (!next) continue;
19179
+ const [aIndex, bIndex] = next;
19180
+ const state = `${aIndex}:${bIndex}`;
19181
+ if (visited.has(state)) continue;
19182
+ visited.add(state);
19183
+ if (aIndex === a.length && bIndex === b.length) return true;
19184
+ const aChar = a[aIndex];
19185
+ const bChar = b[bIndex];
19186
+ if (aChar === "*") pending.push([aIndex + 1, bIndex]);
19187
+ if (bChar === "*") pending.push([aIndex, bIndex + 1]);
19188
+ if (aChar !== void 0 && bChar !== void 0 && (aChar === "*" || bChar === "*" || aChar === bChar)) pending.push([aChar === "*" ? aIndex : aIndex + 1, bChar === "*" ? bIndex : bIndex + 1]);
19189
+ }
19190
+ return false;
19191
+ }
19192
+ function assertInternalHostsDoNotOverlapProtectedHosts(internalHosts, protectedHosts) {
19193
+ for (const internalHost of internalHosts) {
19194
+ const protectedHost = protectedHosts.find((candidate) => hostnamePatternsOverlap(internalHost, candidate));
19195
+ if (protectedHost) throw new Error(`sandbox.network.allowedInternalHosts pattern "${internalHost}" overlaps external-only host pattern "${protectedHost}"`);
19196
+ }
19197
+ }
19198
+ /**
19013
19199
  * Run a shell command in the guest and throw if it fails. Mirror of
19014
19200
  * `run()` in `snapshot.ts` for the resume-side hook chain — every
19015
19201
  * setup step is essential to a healthy session, so a silent non-zero
@@ -19051,11 +19237,18 @@ async function resumeVm(config) {
19051
19237
  const creds = loadCredentials(agentDir);
19052
19238
  const moltnetConfig = JSON.parse(creds.moltnetJson);
19053
19239
  const apiHost = new URL(moltnetConfig.endpoints.api).hostname;
19054
- const { httpHooks, env: secretEnv } = createHttpHooks({ allowedHosts: [
19240
+ const runtimeAllowedHosts = config.sandboxConfig?.network?.allowedHosts ?? [];
19241
+ const runtimeAllowedInternalHosts = config.sandboxConfig?.network?.allowedInternalHosts ?? [];
19242
+ const protectedExternalHosts = [...new Set([
19055
19243
  ...BASE_ALLOWED_HOSTS,
19056
19244
  apiHost,
19057
19245
  ...config.extraAllowedHosts ?? []
19058
- ] });
19246
+ ])];
19247
+ assertInternalHostsDoNotOverlapProtectedHosts(runtimeAllowedInternalHosts, protectedExternalHosts);
19248
+ const { httpHooks, env: secretEnv } = createHttpHooks({
19249
+ allowedHosts: [...new Set([...protectedExternalHosts, ...runtimeAllowedHosts])],
19250
+ allowedInternalHosts: runtimeAllowedInternalHosts
19251
+ });
19059
19252
  const vmAgentDir = `/home/agent/.moltnet/${config.agentName}`;
19060
19253
  const vmAgentEnv = {};
19061
19254
  for (const [k, v] of Object.entries(creds.agentEnv)) {
@@ -20145,6 +20338,8 @@ function buildFinalOutputBlock(opts) {
20145
20338
  `\`moltnet_list_task_artifacts\` for the referenced task and download the`,
20146
20339
  `specific CID you need with \`moltnet_download_task_artifact\` before judging`,
20147
20340
  `or continuing that work.`,
20341
+ `For a bound input artifact, omit \`attemptN\` because it has no producing`,
20342
+ `attempt. Pass \`attemptN\` only for an artifact from one exact task attempt.`,
20148
20343
  "",
20149
20344
  `Output shape:`,
20150
20345
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.33.1",
3
+ "version": "0.34.1",
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.1",
40
- "@themoltnet/sdk": "0.119.0"
39
+ "@themoltnet/agent-runtime": "0.35.3",
40
+ "@themoltnet/sdk": "0.120.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@earendil-works/pi-coding-agent": ">=0.74.0",