@poncho-ai/harness 0.43.1 → 0.44.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/dist/index.js CHANGED
@@ -2340,7 +2340,7 @@ var ponchoDocsTool = defineTool({
2340
2340
 
2341
2341
  // src/harness.ts
2342
2342
  import { randomUUID as randomUUID5 } from "crypto";
2343
- import { readFile as readFile8 } from "fs/promises";
2343
+ import { readFile as readFile9 } from "fs/promises";
2344
2344
  import { resolve as resolve11 } from "path";
2345
2345
  import { defineTool as defineTool12, getTextContent as getTextContent2, createLogger as createLogger6, formatError as fmtErr, url as urlColor } from "@poncho-ai/sdk";
2346
2346
 
@@ -2422,6 +2422,23 @@ var deriveUploadKey = (data, mediaType) => {
2422
2422
  const ext = mimeToExt(mediaType);
2423
2423
  return `${hash}${ext}`;
2424
2424
  };
2425
+ var DATA_URI_PREFIX = /^data:[^,]*?;base64,/;
2426
+ var decodeFileInputData = async (data) => {
2427
+ if (data.startsWith("http://") || data.startsWith("https://")) {
2428
+ const resp = await fetch(data);
2429
+ if (!resp.ok) {
2430
+ throw new Error(
2431
+ `uploads: failed to fetch file at ${data}: ${resp.status} ${resp.statusText}`
2432
+ );
2433
+ }
2434
+ return Buffer.from(await resp.arrayBuffer());
2435
+ }
2436
+ const match = data.match(DATA_URI_PREFIX);
2437
+ if (match) {
2438
+ return Buffer.from(data.slice(match[0].length), "base64");
2439
+ }
2440
+ return Buffer.from(data, "base64");
2441
+ };
2425
2442
  var MIME_EXT_MAP = {
2426
2443
  "image/jpeg": ".jpg",
2427
2444
  "image/png": ".png",
@@ -2470,9 +2487,9 @@ var VercelBlobUploadStore = class {
2470
2487
  sdk;
2471
2488
  workingDir;
2472
2489
  access;
2473
- constructor(workingDir, access3 = "public") {
2490
+ constructor(workingDir, access4 = "public") {
2474
2491
  this.workingDir = workingDir;
2475
- this.access = access3;
2492
+ this.access = access4;
2476
2493
  }
2477
2494
  async loadSdk() {
2478
2495
  if (this.sdk) return this.sdk;
@@ -3319,6 +3336,25 @@ var migrations = [
3319
3336
  ON conversations (parent_conversation_id, parent_message_id)
3320
3337
  WHERE parent_message_id IS NOT NULL`
3321
3338
  ]
3339
+ },
3340
+ {
3341
+ version: 7,
3342
+ name: "fix_reminder_scheduled_at_precision",
3343
+ // Postgres maps `REAL` to float4 (4 bytes, ~7 digit precision),
3344
+ // which silently rounds millisecond epochs (13 digits) — every
3345
+ // reminder write+read on Postgres returned a different value than
3346
+ // it stored. SQLite's REAL is float8 (15+ digit precision) so it
3347
+ // was always fine there.
3348
+ //
3349
+ // Convert the Postgres column to BIGINT so future writes are exact
3350
+ // (already-stored rounded values aren't recoverable but they were
3351
+ // never correct in the first place).
3352
+ up: (d) => {
3353
+ if (d === "sqlite") return [];
3354
+ return [
3355
+ `ALTER TABLE reminders ALTER COLUMN scheduled_at TYPE BIGINT USING scheduled_at::bigint`
3356
+ ];
3357
+ }
3322
3358
  }
3323
3359
  ];
3324
3360
 
@@ -3982,9 +4018,9 @@ var SqlStorageEngine = class {
3982
4018
  }
3983
4019
  },
3984
4020
  deleteFile: async (tenantId, path) => {
3985
- const stat3 = await this.vfs.stat(tenantId, path);
3986
- if (!stat3) throw new Error(`ENOENT: no such file or directory, unlink '${path}'`);
3987
- if (stat3.type === "directory") throw new Error(`EISDIR: illegal operation on a directory, unlink '${path}'`);
4021
+ const stat4 = await this.vfs.stat(tenantId, path);
4022
+ if (!stat4) throw new Error(`ENOENT: no such file or directory, unlink '${path}'`);
4023
+ if (stat4.type === "directory") throw new Error(`EISDIR: illegal operation on a directory, unlink '${path}'`);
3988
4024
  await this.executor.run(
3989
4025
  rewrite(
3990
4026
  "DELETE FROM vfs_entries WHERE agent_id = $1 AND tenant_id = $2 AND path = $3",
@@ -4224,7 +4260,10 @@ var SqlStorageEngine = class {
4224
4260
  return {
4225
4261
  id: row.id,
4226
4262
  task: row.task,
4227
- scheduledAt: row.scheduled_at,
4263
+ // Postgres-js returns BIGINT columns as strings to avoid silent
4264
+ // precision loss in JS. Coerce to Number — ms epochs max out at
4265
+ // ~10^16 in year 2286, well under Number.MAX_SAFE_INTEGER (2^53).
4266
+ scheduledAt: Number(row.scheduled_at),
4228
4267
  timezone: row.timezone ?? void 0,
4229
4268
  status: row.status,
4230
4269
  createdAt: new Date(row.created_at).getTime(),
@@ -4232,7 +4271,7 @@ var SqlStorageEngine = class {
4232
4271
  ownerId: row.owner_id ?? void 0,
4233
4272
  tenantId: tid === DEFAULT_TENANT2 ? null : tid,
4234
4273
  recurrence,
4235
- occurrenceCount: row.occurrence_count ?? 0
4274
+ occurrenceCount: Number(row.occurrence_count ?? 0)
4236
4275
  };
4237
4276
  }
4238
4277
  toUint8Array(value) {
@@ -4528,6 +4567,9 @@ function createReminderStoreFromEngine(engine) {
4528
4567
  import { Bash } from "just-bash";
4529
4568
 
4530
4569
  // src/vfs/poncho-fs-adapter.ts
4570
+ import * as nodeFs from "fs/promises";
4571
+ import * as nodeFsSync from "fs";
4572
+ import * as nodePath from "path";
4531
4573
  var MIME_MAP = {
4532
4574
  ".txt": "text/plain",
4533
4575
  ".html": "text/html",
@@ -4584,52 +4626,190 @@ var normalize = (path) => {
4584
4626
  }
4585
4627
  return "/" + out.join("/");
4586
4628
  };
4629
+ var READ_ONLY_ERROR = (path, op) => new Error(`EROFS: read-only mount, ${op} '${path}'`);
4587
4630
  var PonchoFsAdapter = class {
4588
- constructor(engine, tenantId, limits) {
4631
+ constructor(engine, tenantId, limits, mounts = []) {
4589
4632
  this.engine = engine;
4590
4633
  this.tenantId = tenantId;
4591
4634
  this.limits = limits;
4635
+ this.mounts = mounts.map((m) => {
4636
+ const prefix = m.prefix.endsWith("/") ? m.prefix : m.prefix + "/";
4637
+ return {
4638
+ prefix,
4639
+ prefixNoSlash: prefix.slice(0, -1),
4640
+ source: m.source.replace(/\/+$/, "")
4641
+ };
4642
+ });
4643
+ }
4644
+ mounts;
4645
+ /** Find which mount, if any, a normalised VFS path falls under.
4646
+ * Returns the relative path within the mount's source dir (empty string
4647
+ * when the path is exactly the mount root). */
4648
+ routeToMount(np) {
4649
+ for (const m of this.mounts) {
4650
+ if (np === m.prefixNoSlash) return { mount: m, relative: "" };
4651
+ if (np.startsWith(m.prefix)) return { mount: m, relative: np.slice(m.prefix.length) };
4652
+ }
4653
+ return null;
4654
+ }
4655
+ /** Treat `np` as a directory and return mount-root segments that should be
4656
+ * listed as virtual subdirectories. E.g. with mount "/system/", reading
4657
+ * "/" returns ["system"]; reading "/system" goes via routeToMount and
4658
+ * serves from local FS instead. */
4659
+ virtualChildrenAt(np) {
4660
+ const dirPrefix = np === "/" ? "/" : np + "/";
4661
+ const out = [];
4662
+ for (const m of this.mounts) {
4663
+ if (m.prefix.startsWith(dirPrefix) && m.prefix !== dirPrefix) {
4664
+ const remaining = m.prefix.slice(dirPrefix.length);
4665
+ const seg = remaining.split("/")[0];
4666
+ if (seg && !out.includes(seg)) out.push(seg);
4667
+ }
4668
+ }
4669
+ return out;
4670
+ }
4671
+ toLocal(mount, relative) {
4672
+ return nodePath.join(mount.source, relative);
4673
+ }
4674
+ /** Build an FsStat from a node fs.Stats. */
4675
+ toFsStat(s) {
4676
+ return {
4677
+ isFile: s.isFile(),
4678
+ isDirectory: s.isDirectory(),
4679
+ isSymbolicLink: s.isSymbolicLink(),
4680
+ mode: s.mode,
4681
+ size: s.size,
4682
+ mtime: s.mtime
4683
+ };
4684
+ }
4685
+ /** Synthesise a directory stat for a virtual ancestor (e.g. "/system"
4686
+ * when "/system/jobs/" is mounted but "/system" itself isn't a real dir
4687
+ * on disk). Used so `ls /` and `stat /system` work without surprises. */
4688
+ syntheticDirStat() {
4689
+ return {
4690
+ isFile: false,
4691
+ isDirectory: true,
4692
+ isSymbolicLink: false,
4693
+ mode: 493,
4694
+ size: 0,
4695
+ mtime: /* @__PURE__ */ new Date(0)
4696
+ };
4592
4697
  }
4593
4698
  // --- Reads ---
4594
4699
  async readFile(path, _options) {
4595
- const buf = await this.engine.vfs.readFile(this.tenantId, normalize(path));
4700
+ const np = normalize(path);
4701
+ const route = this.routeToMount(np);
4702
+ if (route) {
4703
+ const buf2 = await nodeFs.readFile(this.toLocal(route.mount, route.relative));
4704
+ return buf2.toString("utf8");
4705
+ }
4706
+ const buf = await this.engine.vfs.readFile(this.tenantId, np);
4596
4707
  return new TextDecoder().decode(buf);
4597
4708
  }
4598
4709
  async readFileBuffer(path) {
4599
- return this.engine.vfs.readFile(this.tenantId, normalize(path));
4710
+ const np = normalize(path);
4711
+ const route = this.routeToMount(np);
4712
+ if (route) {
4713
+ const buf = await nodeFs.readFile(this.toLocal(route.mount, route.relative));
4714
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
4715
+ }
4716
+ return this.engine.vfs.readFile(this.tenantId, np);
4600
4717
  }
4601
4718
  async exists(path) {
4602
- const s = await this.engine.vfs.stat(this.tenantId, normalize(path));
4603
- return s !== void 0;
4719
+ const np = normalize(path);
4720
+ const route = this.routeToMount(np);
4721
+ if (route) {
4722
+ try {
4723
+ await nodeFs.access(this.toLocal(route.mount, route.relative));
4724
+ return true;
4725
+ } catch {
4726
+ return false;
4727
+ }
4728
+ }
4729
+ const s = await this.engine.vfs.stat(this.tenantId, np);
4730
+ if (s) return true;
4731
+ return this.virtualChildrenAt(np).length > 0;
4604
4732
  }
4605
4733
  async stat(path) {
4606
4734
  const np = normalize(path);
4735
+ const route = this.routeToMount(np);
4736
+ if (route) {
4737
+ try {
4738
+ const s2 = await nodeFs.stat(this.toLocal(route.mount, route.relative));
4739
+ return this.toFsStat(s2);
4740
+ } catch {
4741
+ throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
4742
+ }
4743
+ }
4607
4744
  const s = await this.engine.vfs.stat(this.tenantId, np);
4608
- if (!s) throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
4609
- return {
4610
- isFile: s.type === "file",
4611
- isDirectory: s.type === "directory",
4612
- isSymbolicLink: s.type === "symlink",
4613
- mode: s.mode,
4614
- size: s.size,
4615
- mtime: new Date(s.updatedAt)
4616
- };
4745
+ if (s) {
4746
+ return {
4747
+ isFile: s.type === "file",
4748
+ isDirectory: s.type === "directory",
4749
+ isSymbolicLink: s.type === "symlink",
4750
+ mode: s.mode,
4751
+ size: s.size,
4752
+ mtime: new Date(s.updatedAt)
4753
+ };
4754
+ }
4755
+ if (this.virtualChildrenAt(np).length > 0) return this.syntheticDirStat();
4756
+ throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
4617
4757
  }
4618
4758
  async readdir(path) {
4619
- const entries = await this.engine.vfs.readdir(this.tenantId, normalize(path));
4620
- return entries.map((e) => e.name);
4759
+ const np = normalize(path);
4760
+ const route = this.routeToMount(np);
4761
+ if (route) {
4762
+ return nodeFs.readdir(this.toLocal(route.mount, route.relative));
4763
+ }
4764
+ let engineNames = [];
4765
+ try {
4766
+ const entries = await this.engine.vfs.readdir(this.tenantId, np);
4767
+ engineNames = entries.map((e) => e.name);
4768
+ } catch {
4769
+ }
4770
+ const virtualSegs = this.virtualChildrenAt(np);
4771
+ if (virtualSegs.length === 0) return engineNames;
4772
+ const merged = new Set(engineNames);
4773
+ for (const seg of virtualSegs) merged.add(seg);
4774
+ return Array.from(merged);
4621
4775
  }
4622
4776
  async readdirWithFileTypes(path) {
4623
- const entries = await this.engine.vfs.readdir(this.tenantId, normalize(path));
4624
- return entries.map((e) => ({
4625
- name: e.name,
4626
- isFile: e.type === "file",
4627
- isDirectory: e.type === "directory",
4628
- isSymbolicLink: e.type === "symlink"
4629
- }));
4777
+ const np = normalize(path);
4778
+ const route = this.routeToMount(np);
4779
+ if (route) {
4780
+ const entries = await nodeFs.readdir(this.toLocal(route.mount, route.relative), { withFileTypes: true });
4781
+ return entries.map((e) => ({
4782
+ name: e.name,
4783
+ isFile: e.isFile(),
4784
+ isDirectory: e.isDirectory(),
4785
+ isSymbolicLink: e.isSymbolicLink()
4786
+ }));
4787
+ }
4788
+ let engineEntries = [];
4789
+ try {
4790
+ const entries = await this.engine.vfs.readdir(this.tenantId, np);
4791
+ engineEntries = entries.map((e) => ({
4792
+ name: e.name,
4793
+ isFile: e.type === "file",
4794
+ isDirectory: e.type === "directory",
4795
+ isSymbolicLink: e.type === "symlink"
4796
+ }));
4797
+ } catch {
4798
+ }
4799
+ const virtualSegs = this.virtualChildrenAt(np);
4800
+ if (virtualSegs.length === 0) return engineEntries;
4801
+ const seen = new Set(engineEntries.map((e) => e.name));
4802
+ for (const seg of virtualSegs) {
4803
+ if (!seen.has(seg)) {
4804
+ engineEntries.push({ name: seg, isFile: false, isDirectory: true, isSymbolicLink: false });
4805
+ }
4806
+ }
4807
+ return engineEntries;
4630
4808
  }
4631
4809
  // --- Writes ---
4632
4810
  async writeFile(path, content, _options) {
4811
+ const np = normalize(path);
4812
+ if (this.routeToMount(np)) throw READ_ONLY_ERROR(path, "writeFile");
4633
4813
  const buf = typeof content === "string" ? new TextEncoder().encode(content) : content;
4634
4814
  if (buf.byteLength > this.limits.maxFileSize) {
4635
4815
  throw new Error(
@@ -4637,17 +4817,22 @@ var PonchoFsAdapter = class {
4637
4817
  );
4638
4818
  }
4639
4819
  const mime = mimeFromExtension(path);
4640
- await this.engine.vfs.writeFile(this.tenantId, normalize(path), buf, mime);
4820
+ await this.engine.vfs.writeFile(this.tenantId, np, buf, mime);
4641
4821
  }
4642
4822
  async appendFile(path, content, _options) {
4823
+ const np = normalize(path);
4824
+ if (this.routeToMount(np)) throw READ_ONLY_ERROR(path, "appendFile");
4643
4825
  const buf = typeof content === "string" ? new TextEncoder().encode(content) : content;
4644
- await this.engine.vfs.appendFile(this.tenantId, normalize(path), buf);
4826
+ await this.engine.vfs.appendFile(this.tenantId, np, buf);
4645
4827
  }
4646
4828
  async mkdir(path, options) {
4647
- await this.engine.vfs.mkdir(this.tenantId, normalize(path), options?.recursive);
4829
+ const np = normalize(path);
4830
+ if (this.routeToMount(np)) throw READ_ONLY_ERROR(path, "mkdir");
4831
+ await this.engine.vfs.mkdir(this.tenantId, np, options?.recursive);
4648
4832
  }
4649
4833
  async rm(path, options) {
4650
4834
  const np = normalize(path);
4835
+ if (this.routeToMount(np)) throw READ_ONLY_ERROR(path, "rm");
4651
4836
  const s = await this.engine.vfs.stat(this.tenantId, np);
4652
4837
  if (!s) {
4653
4838
  if (options?.force) return;
@@ -4663,25 +4848,30 @@ var PonchoFsAdapter = class {
4663
4848
  async cp(src, dest, options) {
4664
4849
  const srcNorm = normalize(src);
4665
4850
  const destNorm = normalize(dest);
4666
- const srcStat = await this.engine.vfs.stat(this.tenantId, srcNorm);
4851
+ if (this.routeToMount(destNorm)) throw READ_ONLY_ERROR(dest, "cp");
4852
+ const srcStat = await this.stat(srcNorm).catch(() => null);
4667
4853
  if (!srcStat) throw new Error(`ENOENT: no such file or directory, cp '${src}'`);
4668
- if (srcStat.type === "directory") {
4854
+ if (srcStat.isDirectory) {
4669
4855
  if (!options?.recursive) {
4670
4856
  throw new Error(`EISDIR: cp -r not specified; omitting directory '${src}'`);
4671
4857
  }
4672
4858
  await this.engine.vfs.mkdir(this.tenantId, destNorm, true);
4673
- const entries = await this.engine.vfs.readdir(this.tenantId, srcNorm);
4674
- for (const entry of entries) {
4675
- await this.cp(`${srcNorm}/${entry.name}`, `${destNorm}/${entry.name}`, options);
4859
+ const entries = await this.readdir(srcNorm);
4860
+ for (const name of entries) {
4861
+ await this.cp(`${srcNorm}/${name}`, `${destNorm}/${name}`, options);
4676
4862
  }
4677
4863
  } else {
4678
- const content = await this.engine.vfs.readFile(this.tenantId, srcNorm);
4864
+ const content = await this.readFileBuffer(srcNorm);
4679
4865
  const mime = mimeFromExtension(destNorm);
4680
4866
  await this.engine.vfs.writeFile(this.tenantId, destNorm, content, mime);
4681
4867
  }
4682
4868
  }
4683
4869
  async mv(src, dest) {
4684
- await this.engine.vfs.rename(this.tenantId, normalize(src), normalize(dest));
4870
+ const srcNorm = normalize(src);
4871
+ const destNorm = normalize(dest);
4872
+ if (this.routeToMount(srcNorm)) throw READ_ONLY_ERROR(src, "mv (source)");
4873
+ if (this.routeToMount(destNorm)) throw READ_ONLY_ERROR(dest, "mv (dest)");
4874
+ await this.engine.vfs.rename(this.tenantId, srcNorm, destNorm);
4685
4875
  }
4686
4876
  // --- Path resolution ---
4687
4877
  resolvePath(base, path) {
@@ -4690,8 +4880,22 @@ var PonchoFsAdapter = class {
4690
4880
  }
4691
4881
  async realpath(path) {
4692
4882
  const np = normalize(path);
4883
+ const route = this.routeToMount(np);
4884
+ if (route) {
4885
+ const localResolved = await nodeFs.realpath(this.toLocal(route.mount, route.relative));
4886
+ const localRoot = await nodeFs.realpath(route.mount.source).catch(() => route.mount.source);
4887
+ if (localResolved === localRoot) return route.mount.prefixNoSlash;
4888
+ if (localResolved.startsWith(localRoot + nodePath.sep)) {
4889
+ const rel = localResolved.slice(localRoot.length + 1).split(nodePath.sep).join("/");
4890
+ return `${route.mount.prefix}${rel}`;
4891
+ }
4892
+ return np;
4893
+ }
4693
4894
  const s = await this.engine.vfs.lstat(this.tenantId, np);
4694
- if (!s) throw new Error(`ENOENT: no such file or directory, realpath '${path}'`);
4895
+ if (!s) {
4896
+ if (this.virtualChildrenAt(np).length > 0) return np;
4897
+ throw new Error(`ENOENT: no such file or directory, realpath '${path}'`);
4898
+ }
4695
4899
  if (s.type === "symlink" && s.symlinkTarget) {
4696
4900
  const target = s.symlinkTarget.startsWith("/") ? s.symlinkTarget : normalize(`${np.slice(0, np.lastIndexOf("/"))}/${s.symlinkTarget}`);
4697
4901
  return this.realpath(target);
@@ -4700,39 +4904,92 @@ var PonchoFsAdapter = class {
4700
4904
  }
4701
4905
  // --- Sync: required by just-bash for glob/find ---
4702
4906
  getAllPaths() {
4703
- return this.engine.vfs.listAllPaths(this.tenantId);
4907
+ const enginePaths = this.engine.vfs.listAllPaths(this.tenantId);
4908
+ if (this.mounts.length === 0) return enginePaths;
4909
+ const out = new Set(enginePaths);
4910
+ for (const m of this.mounts) {
4911
+ out.add(m.prefixNoSlash);
4912
+ try {
4913
+ const stack = [
4914
+ { abs: m.source, vfs: m.prefixNoSlash }
4915
+ ];
4916
+ while (stack.length > 0) {
4917
+ const { abs, vfs } = stack.pop();
4918
+ let entries;
4919
+ try {
4920
+ entries = nodeFsSync.readdirSync(abs, { withFileTypes: true });
4921
+ } catch {
4922
+ continue;
4923
+ }
4924
+ for (const e of entries) {
4925
+ const childVfs = `${vfs}/${e.name}`;
4926
+ out.add(childVfs);
4927
+ if (e.isDirectory()) {
4928
+ stack.push({ abs: nodePath.join(abs, e.name), vfs: childVfs });
4929
+ }
4930
+ }
4931
+ }
4932
+ } catch {
4933
+ }
4934
+ }
4935
+ return Array.from(out);
4704
4936
  }
4705
4937
  // --- Metadata ---
4706
4938
  async chmod(path, mode) {
4707
- await this.engine.vfs.chmod(this.tenantId, normalize(path), mode);
4939
+ const np = normalize(path);
4940
+ if (this.routeToMount(np)) throw READ_ONLY_ERROR(path, "chmod");
4941
+ await this.engine.vfs.chmod(this.tenantId, np, mode);
4708
4942
  }
4709
4943
  async utimes(path, _atime, mtime) {
4710
- await this.engine.vfs.utimes(this.tenantId, normalize(path), mtime);
4944
+ const np = normalize(path);
4945
+ if (this.routeToMount(np)) throw READ_ONLY_ERROR(path, "utimes");
4946
+ await this.engine.vfs.utimes(this.tenantId, np, mtime);
4711
4947
  }
4712
4948
  // --- Symlinks ---
4713
4949
  async symlink(target, linkPath) {
4714
- await this.engine.vfs.symlink(this.tenantId, target, normalize(linkPath));
4950
+ const np = normalize(linkPath);
4951
+ if (this.routeToMount(np)) throw READ_ONLY_ERROR(linkPath, "symlink");
4952
+ await this.engine.vfs.symlink(this.tenantId, target, np);
4715
4953
  }
4716
4954
  async link(existingPath, newPath) {
4717
- const content = await this.engine.vfs.readFile(this.tenantId, normalize(existingPath));
4955
+ const npNew = normalize(newPath);
4956
+ if (this.routeToMount(npNew)) throw READ_ONLY_ERROR(newPath, "link");
4957
+ const content = await this.readFileBuffer(existingPath);
4718
4958
  const mime = mimeFromExtension(newPath);
4719
- await this.engine.vfs.writeFile(this.tenantId, normalize(newPath), content, mime);
4959
+ await this.engine.vfs.writeFile(this.tenantId, npNew, content, mime);
4720
4960
  }
4721
4961
  async readlink(path) {
4722
- return this.engine.vfs.readlink(this.tenantId, normalize(path));
4962
+ const np = normalize(path);
4963
+ const route = this.routeToMount(np);
4964
+ if (route) {
4965
+ return nodeFs.readlink(this.toLocal(route.mount, route.relative));
4966
+ }
4967
+ return this.engine.vfs.readlink(this.tenantId, np);
4723
4968
  }
4724
4969
  async lstat(path) {
4725
4970
  const np = normalize(path);
4971
+ const route = this.routeToMount(np);
4972
+ if (route) {
4973
+ try {
4974
+ const s2 = await nodeFs.lstat(this.toLocal(route.mount, route.relative));
4975
+ return this.toFsStat(s2);
4976
+ } catch {
4977
+ throw new Error(`ENOENT: no such file or directory, lstat '${path}'`);
4978
+ }
4979
+ }
4726
4980
  const s = await this.engine.vfs.lstat(this.tenantId, np);
4727
- if (!s) throw new Error(`ENOENT: no such file or directory, lstat '${path}'`);
4728
- return {
4729
- isFile: s.type === "file",
4730
- isDirectory: s.type === "directory",
4731
- isSymbolicLink: s.type === "symlink",
4732
- mode: s.mode,
4733
- size: s.size,
4734
- mtime: new Date(s.updatedAt)
4735
- };
4981
+ if (s) {
4982
+ return {
4983
+ isFile: s.type === "file",
4984
+ isDirectory: s.type === "directory",
4985
+ isSymbolicLink: s.type === "symlink",
4986
+ mode: s.mode,
4987
+ size: s.size,
4988
+ mtime: new Date(s.updatedAt)
4989
+ };
4990
+ }
4991
+ if (this.virtualChildrenAt(np).length > 0) return this.syntheticDirStat();
4992
+ throw new Error(`ENOENT: no such file or directory, lstat '${path}'`);
4736
4993
  }
4737
4994
  };
4738
4995
 
@@ -4909,21 +5166,23 @@ function toBashOptions(cfg, network) {
4909
5166
  return opts;
4910
5167
  }
4911
5168
  var BashEnvironmentManager = class {
4912
- constructor(engine, limits, workingDir, bashConfig, network) {
5169
+ constructor(engine, limits, workingDir, bashConfig, network, virtualMounts = []) {
4913
5170
  this.engine = engine;
4914
5171
  this.limits = limits;
4915
5172
  this.workingDir = workingDir;
4916
5173
  this.bashOptions = toBashOptions(bashConfig, network);
5174
+ this.virtualMounts = virtualMounts;
4917
5175
  }
4918
5176
  environments = /* @__PURE__ */ new Map();
4919
5177
  filesystems = /* @__PURE__ */ new Map();
4920
5178
  workingDir;
4921
5179
  bashOptions;
5180
+ virtualMounts;
4922
5181
  /** Return the combined IFileSystem (VFS + optional /project mount) for a tenant. */
4923
5182
  getFs(tenantId) {
4924
5183
  let fs = this.filesystems.get(tenantId);
4925
5184
  if (!fs) {
4926
- const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits);
5185
+ const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits, this.virtualMounts);
4927
5186
  fs = createBashFs(adapter, this.workingDir);
4928
5187
  this.filesystems.set(tenantId, fs);
4929
5188
  }
@@ -4943,7 +5202,7 @@ var BashEnvironmentManager = class {
4943
5202
  return bash;
4944
5203
  }
4945
5204
  getAdapter(tenantId) {
4946
- return new PonchoFsAdapter(this.engine, tenantId, this.limits);
5205
+ return new PonchoFsAdapter(this.engine, tenantId, this.limits, this.virtualMounts);
4947
5206
  }
4948
5207
  /** Refresh the PostgreSQL path cache before a bash.exec() call. */
4949
5208
  async refreshPathCache(tenantId) {
@@ -5062,8 +5321,8 @@ var createReadFileTool = (getFs) => defineTool3({
5062
5321
  if (!await fs.exists(filePath)) {
5063
5322
  throw new Error(`File not found: ${filePath}`);
5064
5323
  }
5065
- const stat3 = await fs.stat(filePath);
5066
- if (stat3.isDirectory) {
5324
+ const stat4 = await fs.stat(filePath);
5325
+ if (stat4.isDirectory) {
5067
5326
  throw new Error(`${filePath} is a directory, not a file`);
5068
5327
  }
5069
5328
  const mediaType = mimeFromPath(filePath) ?? "application/octet-stream";
@@ -5115,8 +5374,8 @@ var createEditFileTool = (getFs) => defineTool4({
5115
5374
  const tenantId = context.tenantId ?? "__default__";
5116
5375
  const fs = getFs(tenantId);
5117
5376
  if (!await fs.exists(filePath)) throw new Error(`File not found: ${filePath}`);
5118
- const stat3 = await fs.stat(filePath);
5119
- if (stat3.isDirectory) throw new Error(`${filePath} is a directory`);
5377
+ const stat4 = await fs.stat(filePath);
5378
+ if (stat4.isDirectory) throw new Error(`${filePath} is a directory`);
5120
5379
  const content = await fs.readFile(filePath);
5121
5380
  const first = content.indexOf(oldStr);
5122
5381
  if (first === -1) {
@@ -5611,7 +5870,7 @@ var createTodoTools = (store) => {
5611
5870
 
5612
5871
  // src/secrets-store.ts
5613
5872
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, createHash as createHash3 } from "crypto";
5614
- import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
5873
+ import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
5615
5874
  import { dirname as dirname3, resolve as resolve7 } from "path";
5616
5875
  function deriveKey(signingKey) {
5617
5876
  return createHash3("sha256").update("poncho-secrets-v1:" + signingKey).digest();
@@ -5657,7 +5916,7 @@ var FileSecretsStore = class {
5657
5916
  }
5658
5917
  async readAll(tenantId) {
5659
5918
  try {
5660
- const raw = await readFile5(await this.filePath(tenantId), "utf8");
5919
+ const raw = await readFile6(await this.filePath(tenantId), "utf8");
5661
5920
  return JSON.parse(raw);
5662
5921
  } catch {
5663
5922
  return {};
@@ -6567,7 +6826,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
6567
6826
  // src/openai-codex-auth.ts
6568
6827
  import { homedir as homedir3 } from "os";
6569
6828
  import { dirname as dirname4, resolve as resolve8 } from "path";
6570
- import { mkdir as mkdir4, readFile as readFile6, chmod, writeFile as writeFile5, rm as rm3 } from "fs/promises";
6829
+ import { mkdir as mkdir4, readFile as readFile7, chmod, writeFile as writeFile5, rm as rm3 } from "fs/promises";
6571
6830
  var OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
6572
6831
  var OPENAI_AUTH_ISSUER = "https://auth.openai.com";
6573
6832
  var REFRESH_TOKEN_GRACE_MS = 5 * 60 * 1e3;
@@ -6606,7 +6865,7 @@ var getOpenAICodexAuthFilePath = (config) => {
6606
6865
  var readOpenAICodexSession = async (config) => {
6607
6866
  const filePath = getOpenAICodexAuthFilePath(config);
6608
6867
  try {
6609
- const content = await readFile6(filePath, "utf8");
6868
+ const content = await readFile7(filePath, "utf8");
6610
6869
  const parsed = JSON.parse(content);
6611
6870
  if (typeof parsed.refreshToken !== "string" || parsed.refreshToken.length === 0) {
6612
6871
  return void 0;
@@ -6941,7 +7200,7 @@ var createModelProvider = (provider, config) => {
6941
7200
  };
6942
7201
 
6943
7202
  // src/skill-context.ts
6944
- import { readFile as readFile7, readdir as readdir2, stat } from "fs/promises";
7203
+ import { readFile as readFile8, readdir as readdir3, stat as stat2 } from "fs/promises";
6945
7204
  import { dirname as dirname5, resolve as resolve9, normalize as normalize2 } from "path";
6946
7205
  import YAML3 from "yaml";
6947
7206
  import { createLogger as createLogger4 } from "@poncho-ai/sdk";
@@ -7022,7 +7281,7 @@ var parseSkillFrontmatter = (content) => {
7022
7281
  };
7023
7282
  };
7024
7283
  var collectSkillManifests = async (directory) => {
7025
- const entries = await readdir2(directory, { withFileTypes: true });
7284
+ const entries = await readdir3(directory, { withFileTypes: true });
7026
7285
  const files = [];
7027
7286
  for (const entry of entries) {
7028
7287
  const fullPath = resolve9(directory, entry.name);
@@ -7030,7 +7289,7 @@ var collectSkillManifests = async (directory) => {
7030
7289
  let isFile = entry.isFile();
7031
7290
  if (entry.isSymbolicLink()) {
7032
7291
  try {
7033
- const s = await stat(fullPath);
7292
+ const s = await stat2(fullPath);
7034
7293
  isDir = s.isDirectory();
7035
7294
  isFile = s.isFile();
7036
7295
  } catch {
@@ -7060,7 +7319,7 @@ var loadSkillMetadata = async (workingDir, extraSkillPaths) => {
7060
7319
  const seen = /* @__PURE__ */ new Set();
7061
7320
  for (const manifest of allManifests) {
7062
7321
  try {
7063
- const content = await readFile7(manifest, "utf8");
7322
+ const content = await readFile8(manifest, "utf8");
7064
7323
  const parsed = parseSkillFrontmatter(content);
7065
7324
  if (parsed && !seen.has(parsed.name)) {
7066
7325
  seen.add(parsed.name);
@@ -7163,7 +7422,7 @@ var mergeSkills = (repoSkills, vfsSkills, onCollision) => {
7163
7422
  var loadSkillInstructions = async (skill, engine) => {
7164
7423
  const raw = skill.source.kind === "vfs" ? decoder.decode(
7165
7424
  await requireEngine(engine).vfs.readFile(skill.source.tenantId, skill.skillPath)
7166
- ) : await readFile7(skill.skillPath, "utf8");
7425
+ ) : await readFile8(skill.skillPath, "utf8");
7167
7426
  const match = raw.match(FRONTMATTER_PATTERN3);
7168
7427
  return match ? match[2].trim() : raw.trim();
7169
7428
  };
@@ -7184,7 +7443,7 @@ var readSkillResource = async (skill, relativePath, engine) => {
7184
7443
  if (!fullPath.startsWith(skill.skillDir)) {
7185
7444
  throw new Error("Path escapes the skill directory");
7186
7445
  }
7187
- return await readFile7(fullPath, "utf8");
7446
+ return await readFile8(fullPath, "utf8");
7188
7447
  };
7189
7448
  var requireEngine = (engine) => {
7190
7449
  if (!engine) {
@@ -7312,8 +7571,8 @@ function convertSchema(schema) {
7312
7571
 
7313
7572
  // src/skill-tools.ts
7314
7573
  import { defineTool as defineTool9 } from "@poncho-ai/sdk";
7315
- import { access as access2, readdir as readdir3, stat as stat2 } from "fs/promises";
7316
- import { extname, normalize as normalize3, resolve as resolve10, sep as sep2 } from "path";
7574
+ import { access as access3, readdir as readdir4, stat as stat3 } from "fs/promises";
7575
+ import { extname, normalize as normalize3, resolve as resolve10, sep as sep3 } from "path";
7317
7576
  import { pathToFileURL } from "url";
7318
7577
  import { createJiti as createJiti2 } from "jiti";
7319
7578
  var findSkill = async (options, tenantId, name) => {
@@ -7498,7 +7757,7 @@ var createSkillTools = (options) => {
7498
7757
  error: `Script "${resolved2.relativePath}" for skill "${name}" is not allowed by policy.`
7499
7758
  };
7500
7759
  }
7501
- await access2(resolved2.fullPath);
7760
+ await access3(resolved2.fullPath);
7502
7761
  const fn2 = await loadRunnableScriptFunction(resolved2.fullPath);
7503
7762
  const output2 = await fn2(payload, {
7504
7763
  scope: "skill",
@@ -7514,7 +7773,7 @@ var createSkillTools = (options) => {
7514
7773
  error: `Script "${resolved.relativePath}" is not allowed by policy.`
7515
7774
  };
7516
7775
  }
7517
- await access2(resolved.fullPath);
7776
+ await access3(resolved.fullPath);
7518
7777
  const fn = await loadRunnableScriptFunction(resolved.fullPath);
7519
7778
  const output = await fn(payload, {
7520
7779
  scope: "agent",
@@ -7534,7 +7793,7 @@ var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", "
7534
7793
  var VFS_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".ts", ".mts"]);
7535
7794
  var listRepoSkillScripts = async (skill, isScriptAllowed) => {
7536
7795
  const scripts = await collectScriptFiles(skill.skillDir);
7537
- return scripts.map((fullPath) => fullPath.slice(skill.skillDir.length + 1).split(sep2).join("/")).filter((relativePath) => relativePath.toLowerCase() !== "skill.md").map(
7796
+ return scripts.map((fullPath) => fullPath.slice(skill.skillDir.length + 1).split(sep3).join("/")).filter((relativePath) => relativePath.toLowerCase() !== "skill.md").map(
7538
7797
  (relativePath) => relativePath.includes("/") ? relativePath : `./${relativePath}`
7539
7798
  ).filter((path) => isScriptAllowed ? isScriptAllowed(skill.name, path) : true).sort();
7540
7799
  };
@@ -7567,7 +7826,7 @@ var listVfsSkillScripts = async (skill, engine, isScriptAllowed) => {
7567
7826
  return found.filter((path) => isScriptAllowed ? isScriptAllowed(skill.name, path) : true).sort();
7568
7827
  };
7569
7828
  var collectScriptFiles = async (directory) => {
7570
- const entries = await readdir3(directory, { withFileTypes: true });
7829
+ const entries = await readdir4(directory, { withFileTypes: true });
7571
7830
  const files = [];
7572
7831
  for (const entry of entries) {
7573
7832
  if (entry.name === "node_modules") continue;
@@ -7576,7 +7835,7 @@ var collectScriptFiles = async (directory) => {
7576
7835
  let isFile = entry.isFile();
7577
7836
  if (entry.isSymbolicLink()) {
7578
7837
  try {
7579
- const s = await stat2(fullPath);
7838
+ const s = await stat3(fullPath);
7580
7839
  isDir = s.isDirectory();
7581
7840
  isFile = s.isFile();
7582
7841
  } catch {
@@ -7598,7 +7857,7 @@ var collectScriptFiles = async (directory) => {
7598
7857
  };
7599
7858
  var normalizeScriptPolicyPath = (relativePath) => {
7600
7859
  const trimmed = relativePath.trim();
7601
- const normalized = normalize3(trimmed).split(sep2).join("/");
7860
+ const normalized = normalize3(trimmed).split(sep3).join("/");
7602
7861
  if (normalized.startsWith("/")) {
7603
7862
  throw new Error("Script path must be relative and within the allowed directory");
7604
7863
  }
@@ -7612,7 +7871,7 @@ var resolveScriptPath = (baseDir, relativePath, containmentDir) => {
7612
7871
  const normalized = normalizeScriptPolicyPath(relativePath);
7613
7872
  const fullPath = resolve10(baseDir, normalized);
7614
7873
  const boundary = resolve10(containmentDir ?? baseDir);
7615
- if (!fullPath.startsWith(`${boundary}${sep2}`) && fullPath !== boundary) {
7874
+ if (!fullPath.startsWith(`${boundary}${sep3}`) && fullPath !== boundary) {
7616
7875
  throw new Error("Script path must stay inside the allowed directory");
7617
7876
  }
7618
7877
  const extension = extname(fullPath).toLowerCase();
@@ -8776,6 +9035,8 @@ var AgentHarness = class _AgentHarness {
8776
9035
  storageEngine;
8777
9036
  /** Bash environment manager (creates per-tenant bash instances). */
8778
9037
  bashManager;
9038
+ /** Read-only virtual mounts overlaid on the VFS. Empty by default. */
9039
+ virtualMounts = [];
8779
9040
  resolveToolAccess(toolName) {
8780
9041
  const tools = this.loadedConfig?.tools;
8781
9042
  if (!tools) return true;
@@ -8789,8 +9050,8 @@ var AgentHarness = class _AgentHarness {
8789
9050
  return true;
8790
9051
  }
8791
9052
  isToolEnabled(name) {
8792
- const access3 = this.resolveToolAccess(name);
8793
- if (access3 === false) return false;
9053
+ const access4 = this.resolveToolAccess(name);
9054
+ if (access4 === false) return false;
8794
9055
  if (name === "write_file" || name === "edit_file" || name === "delete_file" || name === "delete_directory") {
8795
9056
  return this.shouldEnableWriteTool();
8796
9057
  }
@@ -8947,6 +9208,7 @@ var AgentHarness = class _AgentHarness {
8947
9208
  this.storageEngine = options.storageEngine;
8948
9209
  this.injectedStorageEngine = true;
8949
9210
  }
9211
+ this.virtualMounts = options.virtualMounts ?? [];
8950
9212
  if (options.toolDefinitions?.length) {
8951
9213
  this.dispatcher.registerMany(options.toolDefinitions);
8952
9214
  }
@@ -9118,6 +9380,10 @@ var AgentHarness = class _AgentHarness {
9118
9380
  return this.loadedSkills;
9119
9381
  }
9120
9382
  const effectiveTenant = tenantId || "__default__";
9383
+ const engineWithRefresh = this.storageEngine;
9384
+ if (typeof engineWithRefresh.refreshPathCache === "function") {
9385
+ await engineWithRefresh.refreshPathCache(effectiveTenant);
9386
+ }
9121
9387
  const fingerprint = this.computeVfsSkillFingerprint(effectiveTenant);
9122
9388
  const cached = this.skillCache.get(effectiveTenant);
9123
9389
  if (cached && cached.fingerprint === fingerprint) {
@@ -9368,7 +9634,7 @@ var AgentHarness = class _AgentHarness {
9368
9634
  }
9369
9635
  try {
9370
9636
  const agentFilePath = resolve11(this.workingDir, "AGENT.md");
9371
- const rawContent = await readFile8(agentFilePath, "utf8");
9637
+ const rawContent = await readFile9(agentFilePath, "utf8");
9372
9638
  if (rawContent === this.agentFileFingerprint) {
9373
9639
  return false;
9374
9640
  }
@@ -9441,7 +9707,7 @@ var AgentHarness = class _AgentHarness {
9441
9707
  }
9442
9708
  } else {
9443
9709
  const agentFilePath = resolve11(this.workingDir, "AGENT.md");
9444
- const agentRawContent = await readFile8(agentFilePath, "utf8");
9710
+ const agentRawContent = await readFile9(agentFilePath, "utf8");
9445
9711
  this.parsedAgent = parseAgentMarkdown(agentRawContent);
9446
9712
  this.agentFileFingerprint = agentRawContent;
9447
9713
  const identity = await ensureAgentIdentity(this.workingDir);
@@ -9488,7 +9754,8 @@ var AgentHarness = class _AgentHarness {
9488
9754
  { maxFileSize, maxTotalStorage },
9489
9755
  bashWorkingDir,
9490
9756
  config?.bash,
9491
- config?.network
9757
+ config?.network,
9758
+ this.virtualMounts
9492
9759
  );
9493
9760
  this.registerIfMissing(createBashTool(this.bashManager));
9494
9761
  const getFs = (tenantId) => this.bashManager.getFs(tenantId);
@@ -9586,9 +9853,9 @@ var AgentHarness = class _AgentHarness {
9586
9853
  await writeFile6(filePath, json, "utf8");
9587
9854
  },
9588
9855
  async load() {
9589
- const { readFile: readFile9 } = await import("fs/promises");
9856
+ const { readFile: readFile10 } = await import("fs/promises");
9590
9857
  try {
9591
- return await readFile9(filePath, "utf8");
9858
+ return await readFile10(filePath, "utf8");
9592
9859
  } catch {
9593
9860
  return void 0;
9594
9861
  }
@@ -9607,9 +9874,9 @@ var AgentHarness = class _AgentHarness {
9607
9874
  await writeFile6(filePath, json, "utf8");
9608
9875
  },
9609
9876
  async load() {
9610
- const { readFile: readFile9 } = await import("fs/promises");
9877
+ const { readFile: readFile10 } = await import("fs/promises");
9611
9878
  try {
9612
- return await readFile9(filePath, "utf8");
9879
+ return await readFile10(filePath, "utf8");
9613
9880
  } catch {
9614
9881
  return void 0;
9615
9882
  }
@@ -9623,12 +9890,12 @@ var AgentHarness = class _AgentHarness {
9623
9890
  let browserMod;
9624
9891
  try {
9625
9892
  const { existsSync } = await import("fs");
9626
- const { join, dirname: dirname6 } = await import("path");
9893
+ const { join: join2, dirname: dirname6 } = await import("path");
9627
9894
  const { pathToFileURL: pathToFileURL2 } = await import("url");
9628
9895
  let searchDir = this.workingDir;
9629
9896
  let entryPath;
9630
9897
  for (; ; ) {
9631
- const candidate = join(searchDir, "node_modules", "@poncho-ai", "browser", "dist", "index.js");
9898
+ const candidate = join2(searchDir, "node_modules", "@poncho-ai", "browser", "dist", "index.js");
9632
9899
  if (existsSync(candidate)) {
9633
9900
  entryPath = candidate;
9634
9901
  break;
@@ -9949,7 +10216,7 @@ ${this.skillFingerprint}`;
9949
10216
  ];
9950
10217
  for (const file of input.files) {
9951
10218
  if (this.uploadStore) {
9952
- const buf = Buffer.from(file.data, "base64");
10219
+ const buf = await decodeFileInputData(file.data);
9953
10220
  const key = deriveUploadKey(buf, file.mediaType);
9954
10221
  const ref = await this.uploadStore.put(key, buf, file.mediaType);
9955
10222
  parts.push({
@@ -13064,7 +13331,7 @@ var runConversationTurn = async (opts) => {
13064
13331
  if (opts.files && opts.files.length > 0 && opts.harness.uploadStore) {
13065
13332
  const uploadedParts = await Promise.all(
13066
13333
  opts.files.map(async (f) => {
13067
- const buf = Buffer.from(f.data, "base64");
13334
+ const buf = await decodeFileInputData(f.data);
13068
13335
  const key = deriveUploadKey(buf, f.mediaType);
13069
13336
  const ref = await opts.harness.uploadStore.put(key, buf, f.mediaType);
13070
13337
  return {
@@ -13418,6 +13685,7 @@ export {
13418
13685
  createTurnDraftState,
13419
13686
  createUploadStore,
13420
13687
  createWriteTool,
13688
+ decodeFileInputData,
13421
13689
  defaultAgentDefinition,
13422
13690
  defineTool13 as defineTool,
13423
13691
  deleteOpenAICodexSession,