@themoltnet/pi-extension 0.28.0 → 0.29.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
@@ -6,7 +6,8 @@ agent's MoltNet identity fully available inside the sandbox.
6
6
  ## How it works
7
7
 
8
8
  Every pi session boots a lightweight Alpine Linux VM from a cached snapshot.
9
- All file system and shell tools (read/write/edit/bash) execute inside the VM.
9
+ All file system, search, and shell tools (read/write/edit/bash/ls/find/grep)
10
+ execute inside the VM.
10
11
  MoltNet API tools (diary entries, pack ops, reflection) run on the host via
11
12
  the SDK and communicate outbound over HTTP.
12
13
 
@@ -24,7 +25,8 @@ pi + extension $MOLTNET_GUEST_WORKSPACE
24
25
  │ (via gitconfig ssh/allowed_signers
25
26
  │ in VM) /home/agent/.pi/agent/auth.json (pi OAuth)
26
27
 
27
- └─ read/write/edit/bash ─▶ vm.exec() / vm.fs.*
28
+ └─ read/write/edit/bash/ls/find/grep
29
+ ─▶ vm.exec() / vm.fs.*
28
30
  (redirected to VM)
29
31
  ```
30
32
 
@@ -104,6 +106,7 @@ tools can use structured in-process calls rather than shell round-trips.
104
106
  | Tool | Runs in | Mechanism |
105
107
  | ----------------------------------- | ------- | -------------------------------------------------------------------------------- |
106
108
  | `read`, `write`, `edit` | VM | Gondolin VFS — agent's FS view is `$MOLTNET_GUEST_WORKSPACE` |
109
+ | `ls`, `find`, `grep` | VM | Gondolin VFS — search/listing observes the sandboxed workspace |
107
110
  | `bash` | VM | `vm.exec()` — shell runs in the isolated guest |
108
111
  | `user_bash` (human `/bash` command) | VM | Same as agent bash |
109
112
  | `moltnet_pack_get` | Host | TypeScript SDK (`@themoltnet/sdk`) authenticated via injected `moltnet.json` |
package/dist/index.d.ts CHANGED
@@ -181,7 +181,7 @@ export declare interface CreateSubagentToolArgs {
181
181
  agentName: string;
182
182
  /**
183
183
  * Custom tools every subagent inherits (Gondolin-routed
184
- * Read/Write/Edit/Bash + moltnet_* tools, etc). MUST NOT include
184
+ * built-ins + moltnet_* tools, etc). MUST NOT include
185
185
  * the parent's submit-output tool, the parent's `subagent` tool,
186
186
  * or any other parent-only artefact — the caller is responsible
187
187
  * for filtering. The subagent appends its own submit tool.
@@ -762,6 +762,13 @@ declare const Task: Type.TObject<{
762
762
  commit_sha: Type.TOptional<Type.TString>;
763
763
  snapshot_cid: Type.TOptional<Type.TString>;
764
764
  }>>;
765
+ artifact: Type.TOptional<Type.TObject<{
766
+ cid: Type.TString;
767
+ attemptN: Type.TInteger;
768
+ kind: Type.TOptional<Type.TString>;
769
+ title: Type.TOptional<Type.TString>;
770
+ contentType: Type.TOptional<Type.TString>;
771
+ }>>;
765
772
  }>>;
766
773
  correlationId: Type.TUnion<[Type.TString, Type.TNull]>;
767
774
  proposedByAgentId: Type.TUnion<[Type.TString, Type.TNull]>;
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { execFileSync } from "node:child_process";
3
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
3
+ import { cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
4
4
  import path, { join, relative, sep } from "node:path";
5
- import { DefaultResourceLoader, SessionManager, createAgentSession, createBashTool, createBashToolDefinition, createEditTool, createEditToolDefinition, createReadTool, createReadToolDefinition, createSyntheticSourceInfo, createWriteTool, createWriteToolDefinition, defineTool, parseFrontmatter } from "@earendil-works/pi-coding-agent";
5
+ import { DEFAULT_MAX_BYTES, DefaultResourceLoader, SessionManager, createAgentSession, createBashTool, createBashToolDefinition, createEditTool, createEditToolDefinition, createFindTool, createFindToolDefinition, createGrepTool, createGrepToolDefinition, createLsTool, createLsToolDefinition, createReadTool, createReadToolDefinition, createSyntheticSourceInfo, createWriteTool, createWriteToolDefinition, defineTool, formatSize, parseFrontmatter, truncateHead, truncateLine } from "@earendil-works/pi-coding-agent";
6
6
  import { createHash } from "node:crypto";
7
7
  import { Readable } from "node:stream";
8
8
  import crypto, { createHash as createHash$1 } from "crypto";
9
- import { readFile } from "node:fs/promises";
9
+ import { readFile, realpath, stat } from "node:fs/promises";
10
10
  import { homedir } from "node:os";
11
+ import { pipeline } from "node:stream/promises";
11
12
  import { Type, getModel } from "@earendil-works/pi-ai";
12
13
  import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, loadGuestAssets } from "@earendil-works/gondolin";
13
14
  import { parseEnv } from "node:util";
@@ -2308,6 +2309,55 @@ var claimTask = (options) => (options.client ?? client).post({
2308
2309
  }
2309
2310
  });
2310
2311
  /**
2312
+ * List task artifact metadata for the current team.
2313
+ */
2314
+ var listTaskArtifacts = (options) => (options.client ?? client).get({
2315
+ security: [
2316
+ {
2317
+ scheme: "bearer",
2318
+ type: "http"
2319
+ },
2320
+ {
2321
+ name: "X-Moltnet-Session-Token",
2322
+ type: "apiKey"
2323
+ },
2324
+ {
2325
+ in: "cookie",
2326
+ name: "ory_kratos_session",
2327
+ type: "apiKey"
2328
+ }
2329
+ ],
2330
+ url: "/tasks/{taskId}/artifacts",
2331
+ ...options
2332
+ });
2333
+ /**
2334
+ * Upload immutable content-addressed artifact content for a task attempt.
2335
+ */
2336
+ var uploadTaskArtifact = (options) => (options.client ?? client).put({
2337
+ bodySerializer: null,
2338
+ security: [
2339
+ {
2340
+ scheme: "bearer",
2341
+ type: "http"
2342
+ },
2343
+ {
2344
+ name: "X-Moltnet-Session-Token",
2345
+ type: "apiKey"
2346
+ },
2347
+ {
2348
+ in: "cookie",
2349
+ name: "ory_kratos_session",
2350
+ type: "apiKey"
2351
+ }
2352
+ ],
2353
+ url: "/tasks/{taskId}/attempts/{attemptN}/artifacts",
2354
+ ...options,
2355
+ headers: {
2356
+ "Content-Type": "application/octet-stream",
2357
+ ...options.headers
2358
+ }
2359
+ });
2360
+ /**
2311
2361
  * List teams the caller belongs to.
2312
2362
  */
2313
2363
  var listTeams = (options) => (options?.client ?? client).get({
@@ -10073,6 +10123,97 @@ var VerificationRecord = _Object_({
10073
10123
  $id: "VerificationRecord",
10074
10124
  additionalProperties: false
10075
10125
  });
10126
+ _Object_({
10127
+ artifacts: _Array_(_Object_({
10128
+ id: String$1({ format: "uuid" }),
10129
+ teamId: String$1({ format: "uuid" }),
10130
+ taskId: String$1({ format: "uuid" }),
10131
+ attemptN: Integer({ minimum: 1 }),
10132
+ kind: String$1({
10133
+ minLength: 1,
10134
+ maxLength: 100
10135
+ }),
10136
+ title: String$1({
10137
+ minLength: 1,
10138
+ maxLength: 255
10139
+ }),
10140
+ contentType: String$1({
10141
+ minLength: 1,
10142
+ maxLength: 200
10143
+ }),
10144
+ contentEncoding: Union([String$1({
10145
+ minLength: 1,
10146
+ maxLength: 100
10147
+ }), Null()]),
10148
+ sizeBytes: Integer({ minimum: 0 }),
10149
+ cid: String$1({
10150
+ minLength: 1,
10151
+ maxLength: 100
10152
+ }),
10153
+ createdByAgentId: String$1({ format: "uuid" }),
10154
+ expiresAt: Union([String$1({ format: "date-time" }), Null()]),
10155
+ createdAt: String$1({ format: "date-time" })
10156
+ }, { $id: "TaskArtifact" })),
10157
+ nextCursor: Union([String$1({ minLength: 1 }), Null()])
10158
+ }, { $id: "TaskArtifactList" });
10159
+ _Object_({
10160
+ limit: Optional(Integer({
10161
+ minimum: 1,
10162
+ maximum: 100
10163
+ })),
10164
+ cursor: Optional(String$1({ minLength: 1 }))
10165
+ }, {
10166
+ $id: "ListTaskArtifactsQuery",
10167
+ additionalProperties: false
10168
+ });
10169
+ _Object_({
10170
+ kind: String$1({
10171
+ minLength: 1,
10172
+ maxLength: 100
10173
+ }),
10174
+ title: String$1({
10175
+ minLength: 1,
10176
+ maxLength: 255
10177
+ }),
10178
+ contentType: Optional(String$1({
10179
+ minLength: 1,
10180
+ maxLength: 200
10181
+ })),
10182
+ contentEncoding: Optional(String$1({
10183
+ minLength: 1,
10184
+ maxLength: 100
10185
+ }))
10186
+ }, {
10187
+ $id: "UploadTaskArtifactQuery",
10188
+ additionalProperties: false
10189
+ });
10190
+ String$1({
10191
+ $id: "TaskArtifactContent",
10192
+ description: "Task artifact content stream.",
10193
+ format: "binary"
10194
+ });
10195
+ _Object_({ taskId: String$1({ format: "uuid" }) }, {
10196
+ $id: "TaskArtifactTaskParams",
10197
+ additionalProperties: false
10198
+ });
10199
+ _Object_({
10200
+ taskId: String$1({ format: "uuid" }),
10201
+ attemptN: Integer({ minimum: 1 })
10202
+ }, {
10203
+ $id: "TaskArtifactAttemptParams",
10204
+ additionalProperties: false
10205
+ });
10206
+ _Object_({
10207
+ taskId: String$1({ format: "uuid" }),
10208
+ attemptN: Integer({ minimum: 1 }),
10209
+ cid: String$1({
10210
+ minLength: 1,
10211
+ maxLength: 100
10212
+ })
10213
+ }, {
10214
+ $id: "TaskArtifactContentParams",
10215
+ additionalProperties: false
10216
+ });
10076
10217
  //#endregion
10077
10218
  //#region ../../node_modules/.pnpm/multiformats@13.4.2/node_modules/multiformats/dist/src/codecs/json.js
10078
10219
  var textEncoder$2 = new TextEncoder();
@@ -10339,6 +10480,10 @@ var FreeformArtifact = _Object_({
10339
10480
  description: Optional(String$1({ minLength: 1 })),
10340
10481
  url: Optional(String$1({ minLength: 1 })),
10341
10482
  path: Optional(String$1({ minLength: 1 })),
10483
+ cid: Optional(String$1({ minLength: 1 })),
10484
+ contentType: Optional(String$1({ minLength: 1 })),
10485
+ contentEncoding: Optional(String$1({ minLength: 1 })),
10486
+ sizeBytes: Optional(Integer({ minimum: 0 })),
10342
10487
  body: Optional(String$1({ maxLength: 65536 }))
10343
10488
  }, {
10344
10489
  $id: "FreeformArtifact",
@@ -13818,7 +13963,23 @@ var TaskRef = _Object_({
13818
13963
  url: Optional(String$1()),
13819
13964
  commit_sha: Optional(String$1()),
13820
13965
  snapshot_cid: Optional(Cid)
13821
- }))
13966
+ })),
13967
+ artifact: Optional(_Object_({
13968
+ cid: Cid,
13969
+ attemptN: Integer({ minimum: 1 }),
13970
+ kind: Optional(String$1({
13971
+ minLength: 1,
13972
+ maxLength: 100
13973
+ })),
13974
+ title: Optional(String$1({
13975
+ minLength: 1,
13976
+ maxLength: 255
13977
+ })),
13978
+ contentType: Optional(String$1({
13979
+ minLength: 1,
13980
+ maxLength: 200
13981
+ }))
13982
+ }, { additionalProperties: false }))
13822
13983
  }, {
13823
13984
  $id: "TaskRef",
13824
13985
  additionalProperties: false
@@ -14163,6 +14324,7 @@ var TaskBuilder = class {
14163
14324
  message: "reference is missing required outputCid"
14164
14325
  }]);
14165
14326
  ref = {
14327
+ ...s,
14166
14328
  taskId: s.taskId ?? null,
14167
14329
  outputCid: s.outputCid,
14168
14330
  role
@@ -14172,6 +14334,60 @@ var TaskBuilder = class {
14172
14334
  return this;
14173
14335
  }
14174
14336
  /**
14337
+ * Add a reference to a persistent task artifact while retaining the accepted
14338
+ * output CID as the provenance anchor.
14339
+ *
14340
+ * @param source - A result reader, raw artifact reference, or `TaskRef`.
14341
+ * @param role - The role the referenced artifact plays.
14342
+ * @returns This builder, for chaining.
14343
+ * @throws {TaskBuildError} when output or artifact CID is missing.
14344
+ */
14345
+ artifactReference(source, role) {
14346
+ let ref;
14347
+ if ("artifactRef" in source && typeof source.artifactRef === "function") ref = source.artifactRef(role);
14348
+ else if ("artifact" in source && source.artifact?.cid) {
14349
+ if (typeof source.artifact.attemptN !== "number" || !Number.isInteger(source.artifact.attemptN) || source.artifact.attemptN < 1) throw new TaskBuildError([{
14350
+ field: "references/artifact/attemptN",
14351
+ message: "artifact reference is missing required attemptN"
14352
+ }]);
14353
+ ref = {
14354
+ ...source,
14355
+ role
14356
+ };
14357
+ } else {
14358
+ const s = source;
14359
+ const errors = [];
14360
+ if (!s.outputCid) errors.push({
14361
+ field: "references/outputCid",
14362
+ message: "reference is missing required outputCid"
14363
+ });
14364
+ if (!s.artifactCid) errors.push({
14365
+ field: "references/artifact/cid",
14366
+ message: "artifact reference is missing required cid"
14367
+ });
14368
+ if (typeof s.attemptN !== "number" || !Number.isInteger(s.attemptN) || s.attemptN < 1) errors.push({
14369
+ field: "references/artifact/attemptN",
14370
+ message: "artifact reference is missing required attemptN"
14371
+ });
14372
+ if (errors.length > 0) throw new TaskBuildError(errors);
14373
+ const attemptN = s.attemptN;
14374
+ ref = {
14375
+ taskId: s.taskId ?? null,
14376
+ outputCid: s.outputCid,
14377
+ role,
14378
+ artifact: {
14379
+ cid: s.artifactCid,
14380
+ attemptN,
14381
+ ...s.kind ? { kind: s.kind } : {},
14382
+ ...s.title ? { title: s.title } : {},
14383
+ ...s.contentType ? { contentType: s.contentType } : {}
14384
+ }
14385
+ };
14386
+ }
14387
+ this.refs.push(ref);
14388
+ return this;
14389
+ }
14390
+ /**
14175
14391
  * Set the owning team (required by the wire schema).
14176
14392
  *
14177
14393
  * @param teamId - Team UUID.
@@ -14549,6 +14765,34 @@ var TaskResultReader = class {
14549
14765
  role
14550
14766
  };
14551
14767
  }
14768
+ /**
14769
+ * Build a `TaskRef` that anchors a downstream task to this accepted output
14770
+ * and points at one persistent task artifact by CID.
14771
+ *
14772
+ * @param filter - Artifact object or a filter resolved against output artifacts.
14773
+ * @param role - The role this artifact plays in the downstream task.
14774
+ * @returns A `TaskRef` with `artifact.cid` populated.
14775
+ * @throws {TaskResultError} if no matching artifact has a CID.
14776
+ */
14777
+ artifactRef(filter, role) {
14778
+ const artifact = typeof filter === "object" && "cid" in filter && "kind" in filter ? filter : this.artifact(filter);
14779
+ if (!artifact?.cid) throw new TaskResultError([{
14780
+ field: "artifacts/cid",
14781
+ message: "no matching artifact with a cid"
14782
+ }]);
14783
+ return {
14784
+ taskId: this.taskId,
14785
+ outputCid: this.outputCid,
14786
+ role,
14787
+ artifact: {
14788
+ cid: artifact.cid,
14789
+ attemptN: this.accepted.attemptN,
14790
+ kind: artifact.kind,
14791
+ title: artifact.title,
14792
+ ...artifact.contentType ? { contentType: artifact.contentType } : {}
14793
+ }
14794
+ };
14795
+ }
14552
14796
  };
14553
14797
  /**
14554
14798
  * Validate and construct a {@link TaskResultReader} from a task and its
@@ -14573,6 +14817,63 @@ function createTasksNamespace(context) {
14573
14817
  auth
14574
14818
  }));
14575
14819
  },
14820
+ artifacts: {
14821
+ async upload(path, body, query, options) {
14822
+ return unwrapResult(await uploadTaskArtifact({
14823
+ auth,
14824
+ body,
14825
+ client,
14826
+ duplex: "half",
14827
+ headers: {
14828
+ ...requiredTeamHeaders(options),
14829
+ "content-type": "application/octet-stream"
14830
+ },
14831
+ path,
14832
+ query
14833
+ }));
14834
+ },
14835
+ async list(taskId, options, query) {
14836
+ return unwrapResult(await listTaskArtifacts({
14837
+ client,
14838
+ auth,
14839
+ headers: requiredTeamHeaders(options),
14840
+ path: { taskId },
14841
+ query
14842
+ })).artifacts;
14843
+ },
14844
+ async listPage(taskId, query, options) {
14845
+ return unwrapResult(await listTaskArtifacts({
14846
+ client,
14847
+ auth,
14848
+ headers: requiredTeamHeaders(options),
14849
+ path: { taskId },
14850
+ query
14851
+ }));
14852
+ },
14853
+ async download(path, options) {
14854
+ const result = await client.request({
14855
+ auth,
14856
+ headers: requiredTeamHeaders(options),
14857
+ method: "GET",
14858
+ parseAs: "stream",
14859
+ path,
14860
+ security: [{
14861
+ scheme: "bearer",
14862
+ type: "http"
14863
+ }],
14864
+ url: "/tasks/{taskId}/attempts/{attemptN}/artifacts/{cid}/content"
14865
+ });
14866
+ const normalizedStream = normalizeDownloadStream(unwrapResult(result));
14867
+ if (normalizedStream) return {
14868
+ artifactId: header(result.response, "x-moltnet-task-artifact-id"),
14869
+ cid: header(result.response, "x-moltnet-task-artifact-cid"),
14870
+ contentEncoding: header(result.response, "x-moltnet-task-artifact-content-encoding"),
14871
+ contentType: header(result.response, "x-moltnet-task-artifact-content-type"),
14872
+ stream: normalizedStream
14873
+ };
14874
+ throw new MoltNetError("Unexpected task artifact download response stream", { code: "INVALID_RESPONSE" });
14875
+ }
14876
+ },
14576
14877
  async list(query, options) {
14577
14878
  return unwrapResult(await listTasks({
14578
14879
  client,
@@ -14741,6 +15042,33 @@ function createTasksNamespace(context) {
14741
15042
  }
14742
15043
  };
14743
15044
  }
15045
+ function header(response, name) {
15046
+ const value = response?.headers.get(name) ?? null;
15047
+ return value === "" ? null : value;
15048
+ }
15049
+ function normalizeDownloadStream(stream) {
15050
+ if (isAsyncIterable(stream)) return stream;
15051
+ if (isReadableStream(stream)) return readableStreamToAsyncIterable(stream);
15052
+ return null;
15053
+ }
15054
+ function isAsyncIterable(value) {
15055
+ return typeof value === "object" && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
15056
+ }
15057
+ function isReadableStream(value) {
15058
+ return typeof value === "object" && value !== null && "getReader" in value && typeof value.getReader === "function";
15059
+ }
15060
+ async function* readableStreamToAsyncIterable(stream) {
15061
+ const reader = stream.getReader();
15062
+ try {
15063
+ while (true) {
15064
+ const result = await reader.read();
15065
+ if (result.done) return;
15066
+ yield result.value;
15067
+ }
15068
+ } finally {
15069
+ reader.releaseLock();
15070
+ }
15071
+ }
14744
15072
  //#endregion
14745
15073
  //#region ../sdk/src/namespaces/teams.ts
14746
15074
  function createTeamsNamespace(context) {
@@ -17212,6 +17540,20 @@ function shouldAutoApproveHostExec(params, config) {
17212
17540
  if (!Array.isArray(policy)) return false;
17213
17541
  return policy.some((rule) => hostExecMatchesAutoApproveRule(params, rule));
17214
17542
  }
17543
+ async function resolveWorkspaceFilePath(cwd, filePath) {
17544
+ const resolved = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(cwd, filePath);
17545
+ const [realCwd, realResolved] = await Promise.all([realpath(cwd), realpath(resolved)]);
17546
+ const rel = path.relative(realCwd, realResolved);
17547
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`task artifact path escapes workspace: ${filePath}`);
17548
+ return realResolved;
17549
+ }
17550
+ async function resolveWorkspaceOutputPath(cwd, filePath) {
17551
+ const resolved = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(cwd, filePath);
17552
+ const [realCwd, realParent] = await Promise.all([realpath(cwd), realpath(path.dirname(resolved))]);
17553
+ const rel = path.relative(realCwd, realParent);
17554
+ if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`task artifact output path escapes workspace: ${filePath}`);
17555
+ return resolved;
17556
+ }
17215
17557
  /**
17216
17558
  * Expand the `taskFilter` shorthand on the diary list/search tools into
17217
17559
  * the matching `task:*` provenance tags emitted by `moltnet_create_entry`
@@ -17696,6 +18038,127 @@ function createMoltNetTools(config) {
17696
18038
  };
17697
18039
  }
17698
18040
  });
18041
+ const uploadTaskArtifact = defineTool({
18042
+ name: "moltnet_upload_task_artifact",
18043
+ label: "Upload MoltNet Task Artifact",
18044
+ description: "Upload a file from the current task workspace as an immutable task artifact. Only available during an active task attempt; the tool attaches the artifact to the active taskId/attemptN and returns metadata including cid, sizeBytes, kind, and title. Use this for large logs, reports, build outputs, screenshots, generated files, or other bytes that should be referenced by CID instead of pasted into structured task output.",
18045
+ parameters: Type.Object({
18046
+ filePath: Type.String({ description: "Path to a file under the current task workspace. Relative paths are resolved from the workspace root." }),
18047
+ kind: Type.String({ description: "Artifact category, e.g. log, report, patch, screenshot, bundle, dataset, trace." }),
18048
+ title: Type.String({ description: "Human-readable artifact title, usually the file name." }),
18049
+ contentType: Type.Optional(Type.String({ description: "MIME type. Defaults to application/octet-stream when omitted." })),
18050
+ contentEncoding: Type.Optional(Type.String({ description: "Optional content encoding if the file is already encoded, e.g. gzip." }))
18051
+ }),
18052
+ async execute(_id, params) {
18053
+ const { agent, teamId } = ensureConnected(config);
18054
+ if (!teamId) throw new Error("moltnet_upload_task_artifact requires a team context");
18055
+ const taskCtx = config.getTaskContext?.() ?? null;
18056
+ if (!taskCtx) throw new Error("moltnet_upload_task_artifact is only available during an active task attempt");
18057
+ const cwd = config.getHostCwd?.() ?? process.cwd();
18058
+ const resolved = await resolveWorkspaceFilePath(cwd, params.filePath);
18059
+ const info = await stat(resolved);
18060
+ if (!info.isFile()) throw new Error(`task artifact path is not a file: ${params.filePath}`);
18061
+ const artifact = await agent.tasks.artifacts.upload({
18062
+ taskId: taskCtx.taskId,
18063
+ attemptN: taskCtx.attemptN
18064
+ }, createReadStream(resolved), {
18065
+ kind: params.kind,
18066
+ title: params.title,
18067
+ contentType: params.contentType ?? "application/octet-stream",
18068
+ contentEncoding: params.contentEncoding
18069
+ }, { teamId });
18070
+ return {
18071
+ content: [{
18072
+ type: "text",
18073
+ text: JSON.stringify({
18074
+ ...artifact,
18075
+ filePath: path.relative(cwd, resolved),
18076
+ localSizeBytes: info.size
18077
+ }, null, 2)
18078
+ }],
18079
+ details: {}
18080
+ };
18081
+ }
18082
+ });
18083
+ const listTaskArtifacts = defineTool({
18084
+ name: "moltnet_list_task_artifacts",
18085
+ label: "List MoltNet Task Artifacts",
18086
+ description: "List immutable artifacts attached to a task, including each artifact CID, attempt number, kind, title, content type, size, uploader, and creation time. Use this when judging or continuing work that references task artifacts.",
18087
+ parameters: Type.Object({
18088
+ taskId: Type.Optional(Type.String({ description: "Task ID. Defaults to the active task when running inside a task attempt." })),
18089
+ limit: Type.Optional(Type.Integer({
18090
+ minimum: 1,
18091
+ maximum: 100,
18092
+ description: "Maximum artifacts to return. Defaults to the server page size."
18093
+ })),
18094
+ cursor: Type.Optional(Type.String({ description: "Pagination cursor returned by a previous moltnet_list_task_artifacts call." }))
18095
+ }),
18096
+ async execute(_id, params) {
18097
+ const { agent, teamId } = ensureConnected(config);
18098
+ if (!teamId) throw new Error("moltnet_list_task_artifacts requires a team context");
18099
+ const taskId = params.taskId ?? config.getTaskContext?.()?.taskId;
18100
+ if (!taskId) throw new Error("moltnet_list_task_artifacts requires taskId outside an active task");
18101
+ const page = await agent.tasks.artifacts.listPage(taskId, {
18102
+ cursor: params.cursor,
18103
+ limit: params.limit
18104
+ }, { teamId });
18105
+ return {
18106
+ content: [{
18107
+ type: "text",
18108
+ text: JSON.stringify(page, null, 2)
18109
+ }],
18110
+ details: {}
18111
+ };
18112
+ }
18113
+ });
18114
+ const downloadTaskArtifact = defineTool({
18115
+ name: "moltnet_download_task_artifact",
18116
+ label: "Download MoltNet Task Artifact",
18117
+ 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.",
18118
+ parameters: Type.Object({
18119
+ taskId: Type.Optional(Type.String({ description: "Task ID. Defaults to the active task when running inside a task attempt." })),
18120
+ attemptN: Type.Integer({
18121
+ minimum: 1,
18122
+ description: "Attempt number that produced the artifact."
18123
+ }),
18124
+ cid: Type.String({
18125
+ minLength: 1,
18126
+ description: "Artifact CID returned by moltnet_list_task_artifacts."
18127
+ }),
18128
+ outputPath: Type.String({ description: "New file path under the current task workspace. The tool refuses to overwrite existing files." })
18129
+ }),
18130
+ async execute(_id, params) {
18131
+ const { agent, teamId } = ensureConnected(config);
18132
+ if (!teamId) throw new Error("moltnet_download_task_artifact requires a team context");
18133
+ const taskId = params.taskId ?? config.getTaskContext?.()?.taskId;
18134
+ if (!taskId) throw new Error("moltnet_download_task_artifact requires taskId outside an active task");
18135
+ const cwd = config.getHostCwd?.() ?? process.cwd();
18136
+ const outputPath = await resolveWorkspaceOutputPath(cwd, params.outputPath);
18137
+ const download = await agent.tasks.artifacts.download({
18138
+ taskId,
18139
+ attemptN: params.attemptN,
18140
+ cid: params.cid
18141
+ }, { teamId });
18142
+ await pipeline(download.stream, createWriteStream(outputPath, { flags: "wx" }));
18143
+ const info = await stat(outputPath);
18144
+ return {
18145
+ content: [{
18146
+ type: "text",
18147
+ text: JSON.stringify({
18148
+ taskId,
18149
+ attemptN: params.attemptN,
18150
+ cid: params.cid,
18151
+ artifactId: download.artifactId,
18152
+ contentType: download.contentType,
18153
+ contentEncoding: download.contentEncoding,
18154
+ outputPath: path.relative(cwd, outputPath),
18155
+ sizeBytes: info.size
18156
+ }, null, 2)
18157
+ }],
18158
+ details: {}
18159
+ };
18160
+ }
18161
+ });
17699
18162
  const reviewSessionErrors = defineTool({
17700
18163
  name: "moltnet_review_session_errors",
17701
18164
  label: "Review Session Tool Errors",
@@ -17745,6 +18208,9 @@ function createMoltNetTools(config) {
17745
18208
  getTask,
17746
18209
  listTaskAttempts,
17747
18210
  listTaskMessages,
18211
+ uploadTaskArtifact,
18212
+ listTaskArtifacts,
18213
+ downloadTaskArtifact,
17748
18214
  reviewSessionErrors,
17749
18215
  defineTool({
17750
18216
  name: "moltnet_host_exec",
@@ -18559,11 +19025,13 @@ function rewriteMoltnetJsonPaths(moltnetJson, vmAgentDir, vmSshDir, githubAppPem
18559
19025
  //#region src/tool-operations.ts
18560
19026
  /**
18561
19027
  * Gondolin tool operations: redirect pi's built-in tool operations
18562
- * (read, write, edit, bash) to execute inside the VM.
19028
+ * (read, write, edit, bash, ls, find, grep) to execute inside the VM.
18563
19029
  *
18564
19030
  * Follows the same pattern as upstream pi-gondolin.ts — pi's tool factories
18565
19031
  * accept an `operations` object that provides the underlying I/O.
18566
19032
  */
19033
+ var DEFAULT_GREP_LIMIT = 100;
19034
+ var GREP_MAX_FILE_SIZE = "2M";
18567
19035
  function shQuote(s) {
18568
19036
  return "'" + s.replace(/'/g, "'\\''") + "'";
18569
19037
  }
@@ -18573,6 +19041,18 @@ function normalizeGuestPath(p) {
18573
19041
  function isSameOrInsidePosixPath(candidate, root) {
18574
19042
  return candidate === root || candidate.startsWith(`${root}/`);
18575
19043
  }
19044
+ function resolveLocalPath(localCwd, inputPath) {
19045
+ return path.isAbsolute(inputPath) ? inputPath : path.resolve(localCwd, inputPath);
19046
+ }
19047
+ function toHostToolPath(localCwd, guestWorkspace, guestPath) {
19048
+ const normalizedGuestWorkspace = normalizeGuestPath(guestWorkspace);
19049
+ const normalizedGuestPath = normalizeGuestPath(guestPath);
19050
+ if (isSameOrInsidePosixPath(normalizedGuestPath, normalizedGuestWorkspace)) {
19051
+ const rel = path.posix.relative(normalizedGuestWorkspace, normalizedGuestPath);
19052
+ return rel ? path.join(localCwd, ...rel.split("/")) : localCwd;
19053
+ }
19054
+ return normalizedGuestPath;
19055
+ }
18576
19056
  /**
18577
19057
  * Map a host-side absolute path to a guest-side workspace path.
18578
19058
  * Throws if the path escapes the workspace.
@@ -18592,17 +19072,10 @@ function toGuestPath(localCwd, localPath, guestWorkspace) {
18592
19072
  function createGondolinReadOps(vm, localCwd, guestWorkspace) {
18593
19073
  return {
18594
19074
  readFile: async (p) => {
18595
- const r = await vm.exec(["/bin/cat", toGuestPath(localCwd, p, guestWorkspace)]);
18596
- if (!r.ok) throw new Error(`cat failed (${r.exitCode}): ${r.stderr}`);
18597
- return r.stdoutBuffer;
18598
- },
18599
- access: async (p) => {
18600
- if (!(await vm.exec([
18601
- "/bin/sh",
18602
- "-lc",
18603
- `test -r ${shQuote(toGuestPath(localCwd, p, guestWorkspace))}`
18604
- ])).ok) throw new Error(`not readable: ${p}`);
19075
+ const content = await vm.fs.readFile(toGuestPath(localCwd, p, guestWorkspace));
19076
+ return typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
18605
19077
  },
19078
+ access: async (p) => vm.fs.access(toGuestPath(localCwd, p, guestWorkspace)),
18606
19079
  detectImageMimeType: async (p) => {
18607
19080
  try {
18608
19081
  const r = await vm.exec([
@@ -18660,6 +19133,253 @@ function createGondolinEditOps(vm, localCwd, guestWorkspace) {
18660
19133
  writeFile: w.writeFile
18661
19134
  };
18662
19135
  }
19136
+ function createGondolinLsOps(vm, localCwd, guestWorkspace) {
19137
+ return {
19138
+ exists: async (p) => {
19139
+ try {
19140
+ await vm.fs.access(toGuestPath(localCwd, p, guestWorkspace));
19141
+ return true;
19142
+ } catch {
19143
+ return false;
19144
+ }
19145
+ },
19146
+ stat: async (p) => vm.fs.stat(toGuestPath(localCwd, p, guestWorkspace)),
19147
+ readdir: async (p) => vm.fs.listDir(toGuestPath(localCwd, p, guestWorkspace))
19148
+ };
19149
+ }
19150
+ async function walkGuestFiles(vm, root, visit, signal) {
19151
+ if (signal?.aborted) throw new Error("Operation aborted");
19152
+ if (!(await vm.fs.stat(root, { signal })).isDirectory()) return visit(root, path.posix.basename(root));
19153
+ const walkDirectory = async (dir, relativeDir) => {
19154
+ if (signal?.aborted) throw new Error("Operation aborted");
19155
+ const entries = await vm.fs.listDir(dir, { signal });
19156
+ for (const entry of entries) {
19157
+ if (entry === ".git" || entry === "node_modules") continue;
19158
+ const guestPath = path.posix.join(dir, entry);
19159
+ const relativePath = relativeDir ? path.posix.join(relativeDir, entry) : entry;
19160
+ let entryStat;
19161
+ try {
19162
+ entryStat = await vm.fs.stat(guestPath, { signal });
19163
+ } catch {
19164
+ continue;
19165
+ }
19166
+ if (entryStat.isDirectory()) {
19167
+ if (!await walkDirectory(guestPath, relativePath)) return false;
19168
+ } else if (!await visit(guestPath, relativePath)) return false;
19169
+ }
19170
+ return true;
19171
+ };
19172
+ return walkDirectory(root, "");
19173
+ }
19174
+ function matchesGlob(relativePath, pattern) {
19175
+ return path.posix.matchesGlob(relativePath, pattern);
19176
+ }
19177
+ function matchesToolGlob(relativePath, pattern) {
19178
+ const normalizedPattern = normalizeGuestPath(pattern);
19179
+ if (normalizedPattern.includes("/")) return matchesGlob(relativePath, normalizedPattern) || matchesGlob(relativePath, `**/${normalizedPattern}`);
19180
+ return matchesGlob(path.posix.basename(relativePath), normalizedPattern);
19181
+ }
19182
+ function createGondolinFindOps(vm, localCwd, guestWorkspace) {
19183
+ return {
19184
+ exists: async (p) => {
19185
+ try {
19186
+ await vm.fs.access(toGuestPath(localCwd, p, guestWorkspace));
19187
+ return true;
19188
+ } catch {
19189
+ return false;
19190
+ }
19191
+ },
19192
+ glob: async (pattern, cwd, options) => {
19193
+ const root = toGuestPath(localCwd, cwd, guestWorkspace);
19194
+ const results = [];
19195
+ await walkGuestFiles(vm, root, (guestPath, relativePath) => {
19196
+ if (results.length >= options.limit) return Promise.resolve(false);
19197
+ if (options.ignore.some((ignore) => matchesToolGlob(relativePath, ignore))) return Promise.resolve(true);
19198
+ if (matchesToolGlob(relativePath, pattern)) results.push(toHostToolPath(localCwd, guestWorkspace, guestPath));
19199
+ return Promise.resolve(results.length < options.limit);
19200
+ });
19201
+ return results;
19202
+ }
19203
+ };
19204
+ }
19205
+ function appendGrepBlock(params) {
19206
+ let linesTruncated = false;
19207
+ const start = params.contextLines > 0 ? Math.max(0, params.lineIndex - params.contextLines) : params.lineIndex;
19208
+ const end = params.contextLines > 0 ? Math.min(params.lines.length - 1, params.lineIndex + params.contextLines) : params.lineIndex;
19209
+ for (let index = start; index <= end; index++) {
19210
+ const { text, wasTruncated } = truncateLine((params.lines[index] ?? "").replace(/\r/g, ""));
19211
+ if (wasTruncated) linesTruncated = true;
19212
+ const separator = index === params.lineIndex ? ":" : "-";
19213
+ params.outputLines.push(`${params.relativePath}${separator}${index + 1}${separator} ${text}`);
19214
+ }
19215
+ return linesTruncated;
19216
+ }
19217
+ function parseRgJsonLines(params) {
19218
+ const parts = `${params.carry}${params.chunk}`.split("\n");
19219
+ const carry = parts.pop() ?? "";
19220
+ let limitReached = false;
19221
+ for (const line of parts) {
19222
+ if (!line.trim() || params.matches.length >= params.effectiveLimit) continue;
19223
+ let event;
19224
+ try {
19225
+ event = JSON.parse(line);
19226
+ } catch {
19227
+ continue;
19228
+ }
19229
+ const candidate = event;
19230
+ if (candidate.type !== "match") continue;
19231
+ const guestPath = candidate.data?.path?.text;
19232
+ const lineNumber = candidate.data?.line_number;
19233
+ if (!guestPath || typeof lineNumber !== "number") continue;
19234
+ params.matches.push({
19235
+ guestPath,
19236
+ lineNumber,
19237
+ lineText: candidate.data?.lines?.text
19238
+ });
19239
+ if (params.matches.length >= params.effectiveLimit) {
19240
+ limitReached = true;
19241
+ break;
19242
+ }
19243
+ }
19244
+ return {
19245
+ carry,
19246
+ limitReached
19247
+ };
19248
+ }
19249
+ async function readGuestLines(vm, guestPath, signal) {
19250
+ try {
19251
+ return (await vm.fs.readFile(guestPath, {
19252
+ encoding: "utf8",
19253
+ signal
19254
+ })).replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
19255
+ } catch {
19256
+ return [];
19257
+ }
19258
+ }
19259
+ async function executeGondolinGrep(vm, localCwd, guestWorkspace, params, signal) {
19260
+ const root = toGuestPath(localCwd, resolveLocalPath(localCwd, params.path ?? "."), guestWorkspace);
19261
+ let rootStat;
19262
+ try {
19263
+ rootStat = await vm.fs.stat(root, { signal });
19264
+ } catch {
19265
+ throw new Error(`Path not found: ${resolveLocalPath(localCwd, params.path ?? ".")}`);
19266
+ }
19267
+ const rootIsDirectory = rootStat.isDirectory();
19268
+ const contextLines = params.context && params.context > 0 ? params.context : 0;
19269
+ const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
19270
+ const args = [
19271
+ "--json",
19272
+ "--line-number",
19273
+ "--color=never",
19274
+ "--hidden",
19275
+ "--max-filesize",
19276
+ GREP_MAX_FILE_SIZE
19277
+ ];
19278
+ if (params.ignoreCase) args.push("--ignore-case");
19279
+ if (params.literal) args.push("--fixed-strings");
19280
+ if (params.glob) args.push("--glob", params.glob);
19281
+ args.push("--", params.pattern, root);
19282
+ const outputLines = [];
19283
+ const details = {};
19284
+ const matches = [];
19285
+ let matchLimitReached = false;
19286
+ let linesTruncated = false;
19287
+ let stderr = "";
19288
+ let carry = "";
19289
+ const ac = new AbortController();
19290
+ const onAbort = () => ac.abort();
19291
+ signal?.addEventListener("abort", onAbort, { once: true });
19292
+ try {
19293
+ const proc = vm.exec(["/bin/rg", ...args], {
19294
+ signal: ac.signal,
19295
+ stdout: "pipe",
19296
+ stderr: "pipe"
19297
+ });
19298
+ for await (const chunk of proc.output()) {
19299
+ const text = typeof chunk.data === "string" ? chunk.data : Buffer.from(chunk.data).toString("utf8");
19300
+ if (chunk.stream === "stderr") {
19301
+ stderr += text;
19302
+ continue;
19303
+ }
19304
+ const parsed = parseRgJsonLines({
19305
+ chunk: text,
19306
+ carry,
19307
+ matches,
19308
+ effectiveLimit
19309
+ });
19310
+ carry = parsed.carry;
19311
+ if (parsed.limitReached) {
19312
+ matchLimitReached = true;
19313
+ ac.abort();
19314
+ break;
19315
+ }
19316
+ }
19317
+ const r = await proc;
19318
+ if (!signal?.aborted && !matchLimitReached && r.exitCode !== 0 && r.exitCode !== 1) throw new Error(stderr.trim() || `ripgrep exited with code ${r.exitCode}`);
19319
+ } catch (err) {
19320
+ if (signal?.aborted) throw new Error("Operation aborted");
19321
+ if (matchLimitReached) {} else throw err;
19322
+ } finally {
19323
+ signal?.removeEventListener("abort", onAbort);
19324
+ }
19325
+ if (matches.length === 0) return {
19326
+ content: [{
19327
+ type: "text",
19328
+ text: "No matches found"
19329
+ }],
19330
+ details: void 0
19331
+ };
19332
+ const fileCache = /* @__PURE__ */ new Map();
19333
+ for (const match of matches) {
19334
+ if (signal?.aborted) throw new Error("Operation aborted");
19335
+ const displayPath = rootIsDirectory ? path.posix.relative(root, match.guestPath) : path.posix.basename(match.guestPath);
19336
+ if (contextLines === 0 && match.lineText !== void 0) {
19337
+ const { text, wasTruncated } = truncateLine(match.lineText.replace(/\r\n/g, "\n").replace(/\r/g, "").replace(/\n$/, ""));
19338
+ if (wasTruncated) linesTruncated = true;
19339
+ outputLines.push(`${displayPath}:${match.lineNumber}: ${text}`);
19340
+ continue;
19341
+ }
19342
+ let lines = fileCache.get(match.guestPath);
19343
+ if (!lines) {
19344
+ lines = await readGuestLines(vm, match.guestPath, signal);
19345
+ fileCache.set(match.guestPath, lines);
19346
+ }
19347
+ if (lines.length === 0) {
19348
+ outputLines.push(`${displayPath}:${match.lineNumber}: (unable to read file)`);
19349
+ continue;
19350
+ }
19351
+ if (appendGrepBlock({
19352
+ outputLines,
19353
+ lines,
19354
+ relativePath: displayPath,
19355
+ lineIndex: match.lineNumber - 1,
19356
+ contextLines
19357
+ })) linesTruncated = true;
19358
+ }
19359
+ const truncation = truncateHead(outputLines.join("\n"), { maxLines: Number.MAX_SAFE_INTEGER });
19360
+ const notices = [];
19361
+ let output = truncation.content;
19362
+ if (matchLimitReached) {
19363
+ details.matchLimitReached = effectiveLimit;
19364
+ notices.push(`${effectiveLimit} matches limit reached`);
19365
+ }
19366
+ if (linesTruncated) {
19367
+ details.linesTruncated = true;
19368
+ notices.push("long lines truncated");
19369
+ }
19370
+ if (truncation.truncated) {
19371
+ details.truncation = truncation;
19372
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
19373
+ }
19374
+ if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
19375
+ return {
19376
+ content: [{
19377
+ type: "text",
19378
+ text: output
19379
+ }],
19380
+ details: Object.keys(details).length > 0 ? details : void 0
19381
+ };
19382
+ }
18663
19383
  function createGondolinBashOps(vm, localCwd, guestWorkspace) {
18664
19384
  return { exec: async (command, cwd, { onData, signal, timeout, env }) => {
18665
19385
  const guestCwd = toGuestPath(localCwd, cwd, guestWorkspace);
@@ -19154,6 +19874,18 @@ function buildFinalOutputBlock(opts) {
19154
19874
  `Your final assistant text before that tool call may explain your work,`,
19155
19875
  `but the submit-tool call itself must be your VERY LAST action.`,
19156
19876
  "",
19877
+ `Task artifacts: when you produce large files, binary files, logs, reports,`,
19878
+ `screenshots, traces, bundles, or datasets, save them in the task workspace`,
19879
+ `and call \`moltnet_upload_task_artifact\` before the submit-output tool.`,
19880
+ `Put the returned artifact CID in the structured output where the schema`,
19881
+ `allows artifact metadata (for example \`artifacts[].cid\`). Do not paste`,
19882
+ `large bytes into structured output.`,
19883
+ "",
19884
+ `Referenced inputs: if this task depends on prior task artifacts, call`,
19885
+ `\`moltnet_list_task_artifacts\` for the referenced task and download the`,
19886
+ `specific CID you need with \`moltnet_download_task_artifact\` before judging`,
19887
+ `or continuing that work.`,
19888
+ "",
19157
19889
  `Output shape:`,
19158
19890
  "",
19159
19891
  "```json",
@@ -24292,7 +25024,7 @@ function subagentToolDescription() {
24292
25024
  "",
24293
25025
  "The subagent starts with no conversation history and only the `task` ",
24294
25026
  "string you provide as its instructions. It runs in the same VM with ",
24295
- "the same tools you have (Gondolin-routed Read/Write/Edit/Bash, ",
25027
+ "the same tools you have (Gondolin-routed built-ins, ",
24296
25028
  "moltnet_* tools), and is expected to call ",
24297
25029
  `\`${SUBAGENT_SUBMIT_TOOL_NAME}\` with a payload matching the named `,
24298
25030
  "contract before its session ends.",
@@ -24784,7 +25516,7 @@ function shouldSkipSeedEntry(sourceEntry, entryName, resolvedTargetDir) {
24784
25516
  *
24785
25517
  * This is the pi-specific task executor. It owns:
24786
25518
  * - VM lifecycle (ensureSnapshot + resumeVm + close)
24787
- * - Gondolin-redirected tool wiring (read/write/edit/bash → VM)
25519
+ * - Gondolin-redirected tool wiring (read/write/edit/bash/ls/find/grep → VM)
24788
25520
  * - MoltNet custom tools (diary entries, pack render/judge, etc.)
24789
25521
  * - pi createAgentSession + event → TaskReporter bridge
24790
25522
  *
@@ -24795,6 +25527,25 @@ function shouldSkipSeedEntry(sourceEntry, entryName, resolvedTargetDir) {
24795
25527
  * `AgentRuntime`.
24796
25528
  */
24797
25529
  var noopTurnEventHandler = () => {};
25530
+ function createGondolinToolDefinitions(config) {
25531
+ const { vm, mountPath, guestWorkspace } = config;
25532
+ const grepTool = createGrepToolDefinition(mountPath);
25533
+ return [
25534
+ createReadToolDefinition(mountPath, { operations: createGondolinReadOps(vm, mountPath, guestWorkspace) }),
25535
+ createWriteToolDefinition(mountPath, { operations: createGondolinWriteOps(vm, mountPath, guestWorkspace) }),
25536
+ createEditToolDefinition(mountPath, { operations: createGondolinEditOps(vm, mountPath, guestWorkspace) }),
25537
+ createBashToolDefinition(mountPath, { operations: createGondolinBashOps(vm, mountPath, guestWorkspace) }),
25538
+ createLsToolDefinition(mountPath, { operations: createGondolinLsOps(vm, mountPath, guestWorkspace) }),
25539
+ createFindToolDefinition(mountPath, { operations: createGondolinFindOps(vm, mountPath, guestWorkspace) }),
25540
+ {
25541
+ ...grepTool,
25542
+ async execute(...args) {
25543
+ const [_id, params, signal] = args;
25544
+ return executeGondolinGrep(vm, mountPath, guestWorkspace, params, signal);
25545
+ }
25546
+ }
25547
+ ];
25548
+ }
24798
25549
  /**
24799
25550
  * Factory that builds a pi-specific `executeTask` function suitable for
24800
25551
  * injection into `AgentRuntime`. The returned function caches the resolved
@@ -25058,12 +25809,11 @@ async function executePiTask(claimedTask, reporter, opts) {
25058
25809
  slugs: injectedContext.injected.map((r) => r.slug)
25059
25810
  });
25060
25811
  if (injectedContext.userInlineSuffix) taskPrompt = `${taskPrompt}\n\n---\n\n${injectedContext.userInlineSuffix}`;
25061
- const gondolinCustomTools = [
25062
- createReadToolDefinition(mountPath, { operations: createGondolinReadOps(managed.vm, mountPath, managed.guestWorkspace) }),
25063
- createWriteToolDefinition(mountPath, { operations: createGondolinWriteOps(managed.vm, mountPath, managed.guestWorkspace) }),
25064
- createEditToolDefinition(mountPath, { operations: createGondolinEditOps(managed.vm, mountPath, managed.guestWorkspace) }),
25065
- createBashToolDefinition(mountPath, { operations: createGondolinBashOps(managed.vm, mountPath, managed.guestWorkspace) })
25066
- ];
25812
+ const gondolinCustomTools = createGondolinToolDefinitions({
25813
+ vm: managed.vm,
25814
+ mountPath,
25815
+ guestWorkspace: managed.guestWorkspace
25816
+ });
25067
25817
  const { handle: submitToolHandle, tools: submitToolDefs } = resolveSubmitTools(task.taskType, {
25068
25818
  model: opts.model,
25069
25819
  input: task.input
@@ -25591,6 +26341,9 @@ function moltnetExtension(pi) {
25591
26341
  const localWrite = createWriteTool(localCwd);
25592
26342
  const localEdit = createEditTool(localCwd);
25593
26343
  const localBash = createBashTool(localCwd);
26344
+ const localLs = createLsTool(localCwd);
26345
+ const localFind = createFindTool(localCwd);
26346
+ const localGrep = createGrepTool(localCwd);
25594
26347
  const initialGuestWorkspace = path.resolve(localCwd);
25595
26348
  let vm = null;
25596
26349
  let guestWorkspace = initialGuestWorkspace;
@@ -25701,6 +26454,24 @@ function moltnetExtension(pi) {
25701
26454
  return createBashTool(localCwd, { operations: createGondolinBashOps(await ensureVm(ctx), localCwd, guestWorkspace) }).execute(id, params, signal, onUpdate);
25702
26455
  }
25703
26456
  });
26457
+ pi.registerTool({
26458
+ ...localLs,
26459
+ async execute(id, params, signal, onUpdate, ctx) {
26460
+ return createLsTool(localCwd, { operations: createGondolinLsOps(await ensureVm(ctx), localCwd, guestWorkspace) }).execute(id, params, signal, onUpdate);
26461
+ }
26462
+ });
26463
+ pi.registerTool({
26464
+ ...localFind,
26465
+ async execute(id, params, signal, onUpdate, ctx) {
26466
+ return createFindTool(localCwd, { operations: createGondolinFindOps(await ensureVm(ctx), localCwd, guestWorkspace) }).execute(id, params, signal, onUpdate);
26467
+ }
26468
+ });
26469
+ pi.registerTool({
26470
+ ...localGrep,
26471
+ async execute(_id, params, signal, _onUpdate, ctx) {
26472
+ return executeGondolinGrep(await ensureVm(ctx), localCwd, guestWorkspace, params, signal);
26473
+ }
26474
+ });
25704
26475
  pi.on("user_bash", (_event, _ctx) => {
25705
26476
  if (!vm) return;
25706
26477
  return { operations: createGondolinBashOps(vm, localCwd, guestWorkspace) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.28.0",
3
+ "version": "0.29.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.31.1",
40
- "@themoltnet/sdk": "0.114.0"
39
+ "@themoltnet/agent-runtime": "0.32.0",
40
+ "@themoltnet/sdk": "0.115.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@earendil-works/pi-coding-agent": ">=0.74.0",