@poncho-ai/harness 0.43.0 → 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
 
@@ -3701,11 +3737,12 @@ var SqlStorageEngine = class {
3701
3737
  const filterTenant = tenantId !== void 0;
3702
3738
  const pattern = `%${query}%`;
3703
3739
  const params = [this.agentId, pattern, pattern];
3740
+ const dataMatch = this.dialect.tag === "postgresql" ? "data::text LIKE $3" : "data LIKE $3";
3704
3741
  let sql = `SELECT id, title, updated_at, created_at, owner_id, tenant_id,
3705
3742
  message_count, parent_conversation_id, parent_message_id,
3706
3743
  has_pending_approvals, channel_meta
3707
3744
  FROM conversations
3708
- WHERE agent_id = $1 AND (title LIKE $2 OR data LIKE $3)`;
3745
+ WHERE agent_id = $1 AND (title LIKE $2 OR ${dataMatch})`;
3709
3746
  if (filterTenant) {
3710
3747
  sql += ` AND tenant_id = $4`;
3711
3748
  params.push(tid);
@@ -3981,9 +4018,9 @@ var SqlStorageEngine = class {
3981
4018
  }
3982
4019
  },
3983
4020
  deleteFile: async (tenantId, path) => {
3984
- const stat3 = await this.vfs.stat(tenantId, path);
3985
- if (!stat3) throw new Error(`ENOENT: no such file or directory, unlink '${path}'`);
3986
- 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}'`);
3987
4024
  await this.executor.run(
3988
4025
  rewrite(
3989
4026
  "DELETE FROM vfs_entries WHERE agent_id = $1 AND tenant_id = $2 AND path = $3",
@@ -4223,7 +4260,10 @@ var SqlStorageEngine = class {
4223
4260
  return {
4224
4261
  id: row.id,
4225
4262
  task: row.task,
4226
- 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),
4227
4267
  timezone: row.timezone ?? void 0,
4228
4268
  status: row.status,
4229
4269
  createdAt: new Date(row.created_at).getTime(),
@@ -4231,7 +4271,7 @@ var SqlStorageEngine = class {
4231
4271
  ownerId: row.owner_id ?? void 0,
4232
4272
  tenantId: tid === DEFAULT_TENANT2 ? null : tid,
4233
4273
  recurrence,
4234
- occurrenceCount: row.occurrence_count ?? 0
4274
+ occurrenceCount: Number(row.occurrence_count ?? 0)
4235
4275
  };
4236
4276
  }
4237
4277
  toUint8Array(value) {
@@ -4527,6 +4567,9 @@ function createReminderStoreFromEngine(engine) {
4527
4567
  import { Bash } from "just-bash";
4528
4568
 
4529
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";
4530
4573
  var MIME_MAP = {
4531
4574
  ".txt": "text/plain",
4532
4575
  ".html": "text/html",
@@ -4583,52 +4626,190 @@ var normalize = (path) => {
4583
4626
  }
4584
4627
  return "/" + out.join("/");
4585
4628
  };
4629
+ var READ_ONLY_ERROR = (path, op) => new Error(`EROFS: read-only mount, ${op} '${path}'`);
4586
4630
  var PonchoFsAdapter = class {
4587
- constructor(engine, tenantId, limits) {
4631
+ constructor(engine, tenantId, limits, mounts = []) {
4588
4632
  this.engine = engine;
4589
4633
  this.tenantId = tenantId;
4590
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
+ };
4591
4697
  }
4592
4698
  // --- Reads ---
4593
4699
  async readFile(path, _options) {
4594
- 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);
4595
4707
  return new TextDecoder().decode(buf);
4596
4708
  }
4597
4709
  async readFileBuffer(path) {
4598
- 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);
4599
4717
  }
4600
4718
  async exists(path) {
4601
- const s = await this.engine.vfs.stat(this.tenantId, normalize(path));
4602
- 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;
4603
4732
  }
4604
4733
  async stat(path) {
4605
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
+ }
4606
4744
  const s = await this.engine.vfs.stat(this.tenantId, np);
4607
- if (!s) throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
4608
- return {
4609
- isFile: s.type === "file",
4610
- isDirectory: s.type === "directory",
4611
- isSymbolicLink: s.type === "symlink",
4612
- mode: s.mode,
4613
- size: s.size,
4614
- mtime: new Date(s.updatedAt)
4615
- };
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}'`);
4616
4757
  }
4617
4758
  async readdir(path) {
4618
- const entries = await this.engine.vfs.readdir(this.tenantId, normalize(path));
4619
- 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);
4620
4775
  }
4621
4776
  async readdirWithFileTypes(path) {
4622
- const entries = await this.engine.vfs.readdir(this.tenantId, normalize(path));
4623
- return entries.map((e) => ({
4624
- name: e.name,
4625
- isFile: e.type === "file",
4626
- isDirectory: e.type === "directory",
4627
- isSymbolicLink: e.type === "symlink"
4628
- }));
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;
4629
4808
  }
4630
4809
  // --- Writes ---
4631
4810
  async writeFile(path, content, _options) {
4811
+ const np = normalize(path);
4812
+ if (this.routeToMount(np)) throw READ_ONLY_ERROR(path, "writeFile");
4632
4813
  const buf = typeof content === "string" ? new TextEncoder().encode(content) : content;
4633
4814
  if (buf.byteLength > this.limits.maxFileSize) {
4634
4815
  throw new Error(
@@ -4636,17 +4817,22 @@ var PonchoFsAdapter = class {
4636
4817
  );
4637
4818
  }
4638
4819
  const mime = mimeFromExtension(path);
4639
- await this.engine.vfs.writeFile(this.tenantId, normalize(path), buf, mime);
4820
+ await this.engine.vfs.writeFile(this.tenantId, np, buf, mime);
4640
4821
  }
4641
4822
  async appendFile(path, content, _options) {
4823
+ const np = normalize(path);
4824
+ if (this.routeToMount(np)) throw READ_ONLY_ERROR(path, "appendFile");
4642
4825
  const buf = typeof content === "string" ? new TextEncoder().encode(content) : content;
4643
- await this.engine.vfs.appendFile(this.tenantId, normalize(path), buf);
4826
+ await this.engine.vfs.appendFile(this.tenantId, np, buf);
4644
4827
  }
4645
4828
  async mkdir(path, options) {
4646
- 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);
4647
4832
  }
4648
4833
  async rm(path, options) {
4649
4834
  const np = normalize(path);
4835
+ if (this.routeToMount(np)) throw READ_ONLY_ERROR(path, "rm");
4650
4836
  const s = await this.engine.vfs.stat(this.tenantId, np);
4651
4837
  if (!s) {
4652
4838
  if (options?.force) return;
@@ -4662,25 +4848,30 @@ var PonchoFsAdapter = class {
4662
4848
  async cp(src, dest, options) {
4663
4849
  const srcNorm = normalize(src);
4664
4850
  const destNorm = normalize(dest);
4665
- 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);
4666
4853
  if (!srcStat) throw new Error(`ENOENT: no such file or directory, cp '${src}'`);
4667
- if (srcStat.type === "directory") {
4854
+ if (srcStat.isDirectory) {
4668
4855
  if (!options?.recursive) {
4669
4856
  throw new Error(`EISDIR: cp -r not specified; omitting directory '${src}'`);
4670
4857
  }
4671
4858
  await this.engine.vfs.mkdir(this.tenantId, destNorm, true);
4672
- const entries = await this.engine.vfs.readdir(this.tenantId, srcNorm);
4673
- for (const entry of entries) {
4674
- 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);
4675
4862
  }
4676
4863
  } else {
4677
- const content = await this.engine.vfs.readFile(this.tenantId, srcNorm);
4864
+ const content = await this.readFileBuffer(srcNorm);
4678
4865
  const mime = mimeFromExtension(destNorm);
4679
4866
  await this.engine.vfs.writeFile(this.tenantId, destNorm, content, mime);
4680
4867
  }
4681
4868
  }
4682
4869
  async mv(src, dest) {
4683
- 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);
4684
4875
  }
4685
4876
  // --- Path resolution ---
4686
4877
  resolvePath(base, path) {
@@ -4689,8 +4880,22 @@ var PonchoFsAdapter = class {
4689
4880
  }
4690
4881
  async realpath(path) {
4691
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
+ }
4692
4894
  const s = await this.engine.vfs.lstat(this.tenantId, np);
4693
- 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
+ }
4694
4899
  if (s.type === "symlink" && s.symlinkTarget) {
4695
4900
  const target = s.symlinkTarget.startsWith("/") ? s.symlinkTarget : normalize(`${np.slice(0, np.lastIndexOf("/"))}/${s.symlinkTarget}`);
4696
4901
  return this.realpath(target);
@@ -4699,39 +4904,92 @@ var PonchoFsAdapter = class {
4699
4904
  }
4700
4905
  // --- Sync: required by just-bash for glob/find ---
4701
4906
  getAllPaths() {
4702
- 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);
4703
4936
  }
4704
4937
  // --- Metadata ---
4705
4938
  async chmod(path, mode) {
4706
- 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);
4707
4942
  }
4708
4943
  async utimes(path, _atime, mtime) {
4709
- 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);
4710
4947
  }
4711
4948
  // --- Symlinks ---
4712
4949
  async symlink(target, linkPath) {
4713
- 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);
4714
4953
  }
4715
4954
  async link(existingPath, newPath) {
4716
- 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);
4717
4958
  const mime = mimeFromExtension(newPath);
4718
- await this.engine.vfs.writeFile(this.tenantId, normalize(newPath), content, mime);
4959
+ await this.engine.vfs.writeFile(this.tenantId, npNew, content, mime);
4719
4960
  }
4720
4961
  async readlink(path) {
4721
- 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);
4722
4968
  }
4723
4969
  async lstat(path) {
4724
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
+ }
4725
4980
  const s = await this.engine.vfs.lstat(this.tenantId, np);
4726
- if (!s) throw new Error(`ENOENT: no such file or directory, lstat '${path}'`);
4727
- return {
4728
- isFile: s.type === "file",
4729
- isDirectory: s.type === "directory",
4730
- isSymbolicLink: s.type === "symlink",
4731
- mode: s.mode,
4732
- size: s.size,
4733
- mtime: new Date(s.updatedAt)
4734
- };
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}'`);
4735
4993
  }
4736
4994
  };
4737
4995
 
@@ -4908,21 +5166,23 @@ function toBashOptions(cfg, network) {
4908
5166
  return opts;
4909
5167
  }
4910
5168
  var BashEnvironmentManager = class {
4911
- constructor(engine, limits, workingDir, bashConfig, network) {
5169
+ constructor(engine, limits, workingDir, bashConfig, network, virtualMounts = []) {
4912
5170
  this.engine = engine;
4913
5171
  this.limits = limits;
4914
5172
  this.workingDir = workingDir;
4915
5173
  this.bashOptions = toBashOptions(bashConfig, network);
5174
+ this.virtualMounts = virtualMounts;
4916
5175
  }
4917
5176
  environments = /* @__PURE__ */ new Map();
4918
5177
  filesystems = /* @__PURE__ */ new Map();
4919
5178
  workingDir;
4920
5179
  bashOptions;
5180
+ virtualMounts;
4921
5181
  /** Return the combined IFileSystem (VFS + optional /project mount) for a tenant. */
4922
5182
  getFs(tenantId) {
4923
5183
  let fs = this.filesystems.get(tenantId);
4924
5184
  if (!fs) {
4925
- const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits);
5185
+ const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits, this.virtualMounts);
4926
5186
  fs = createBashFs(adapter, this.workingDir);
4927
5187
  this.filesystems.set(tenantId, fs);
4928
5188
  }
@@ -4942,7 +5202,7 @@ var BashEnvironmentManager = class {
4942
5202
  return bash;
4943
5203
  }
4944
5204
  getAdapter(tenantId) {
4945
- return new PonchoFsAdapter(this.engine, tenantId, this.limits);
5205
+ return new PonchoFsAdapter(this.engine, tenantId, this.limits, this.virtualMounts);
4946
5206
  }
4947
5207
  /** Refresh the PostgreSQL path cache before a bash.exec() call. */
4948
5208
  async refreshPathCache(tenantId) {
@@ -5061,8 +5321,8 @@ var createReadFileTool = (getFs) => defineTool3({
5061
5321
  if (!await fs.exists(filePath)) {
5062
5322
  throw new Error(`File not found: ${filePath}`);
5063
5323
  }
5064
- const stat3 = await fs.stat(filePath);
5065
- if (stat3.isDirectory) {
5324
+ const stat4 = await fs.stat(filePath);
5325
+ if (stat4.isDirectory) {
5066
5326
  throw new Error(`${filePath} is a directory, not a file`);
5067
5327
  }
5068
5328
  const mediaType = mimeFromPath(filePath) ?? "application/octet-stream";
@@ -5114,8 +5374,8 @@ var createEditFileTool = (getFs) => defineTool4({
5114
5374
  const tenantId = context.tenantId ?? "__default__";
5115
5375
  const fs = getFs(tenantId);
5116
5376
  if (!await fs.exists(filePath)) throw new Error(`File not found: ${filePath}`);
5117
- const stat3 = await fs.stat(filePath);
5118
- 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`);
5119
5379
  const content = await fs.readFile(filePath);
5120
5380
  const first = content.indexOf(oldStr);
5121
5381
  if (first === -1) {
@@ -5610,7 +5870,7 @@ var createTodoTools = (store) => {
5610
5870
 
5611
5871
  // src/secrets-store.ts
5612
5872
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, createHash as createHash3 } from "crypto";
5613
- 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";
5614
5874
  import { dirname as dirname3, resolve as resolve7 } from "path";
5615
5875
  function deriveKey(signingKey) {
5616
5876
  return createHash3("sha256").update("poncho-secrets-v1:" + signingKey).digest();
@@ -5656,7 +5916,7 @@ var FileSecretsStore = class {
5656
5916
  }
5657
5917
  async readAll(tenantId) {
5658
5918
  try {
5659
- const raw = await readFile5(await this.filePath(tenantId), "utf8");
5919
+ const raw = await readFile6(await this.filePath(tenantId), "utf8");
5660
5920
  return JSON.parse(raw);
5661
5921
  } catch {
5662
5922
  return {};
@@ -6566,7 +6826,7 @@ import { createAnthropic } from "@ai-sdk/anthropic";
6566
6826
  // src/openai-codex-auth.ts
6567
6827
  import { homedir as homedir3 } from "os";
6568
6828
  import { dirname as dirname4, resolve as resolve8 } from "path";
6569
- 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";
6570
6830
  var OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
6571
6831
  var OPENAI_AUTH_ISSUER = "https://auth.openai.com";
6572
6832
  var REFRESH_TOKEN_GRACE_MS = 5 * 60 * 1e3;
@@ -6605,7 +6865,7 @@ var getOpenAICodexAuthFilePath = (config) => {
6605
6865
  var readOpenAICodexSession = async (config) => {
6606
6866
  const filePath = getOpenAICodexAuthFilePath(config);
6607
6867
  try {
6608
- const content = await readFile6(filePath, "utf8");
6868
+ const content = await readFile7(filePath, "utf8");
6609
6869
  const parsed = JSON.parse(content);
6610
6870
  if (typeof parsed.refreshToken !== "string" || parsed.refreshToken.length === 0) {
6611
6871
  return void 0;
@@ -6940,7 +7200,7 @@ var createModelProvider = (provider, config) => {
6940
7200
  };
6941
7201
 
6942
7202
  // src/skill-context.ts
6943
- 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";
6944
7204
  import { dirname as dirname5, resolve as resolve9, normalize as normalize2 } from "path";
6945
7205
  import YAML3 from "yaml";
6946
7206
  import { createLogger as createLogger4 } from "@poncho-ai/sdk";
@@ -7021,7 +7281,7 @@ var parseSkillFrontmatter = (content) => {
7021
7281
  };
7022
7282
  };
7023
7283
  var collectSkillManifests = async (directory) => {
7024
- const entries = await readdir2(directory, { withFileTypes: true });
7284
+ const entries = await readdir3(directory, { withFileTypes: true });
7025
7285
  const files = [];
7026
7286
  for (const entry of entries) {
7027
7287
  const fullPath = resolve9(directory, entry.name);
@@ -7029,7 +7289,7 @@ var collectSkillManifests = async (directory) => {
7029
7289
  let isFile = entry.isFile();
7030
7290
  if (entry.isSymbolicLink()) {
7031
7291
  try {
7032
- const s = await stat(fullPath);
7292
+ const s = await stat2(fullPath);
7033
7293
  isDir = s.isDirectory();
7034
7294
  isFile = s.isFile();
7035
7295
  } catch {
@@ -7059,7 +7319,7 @@ var loadSkillMetadata = async (workingDir, extraSkillPaths) => {
7059
7319
  const seen = /* @__PURE__ */ new Set();
7060
7320
  for (const manifest of allManifests) {
7061
7321
  try {
7062
- const content = await readFile7(manifest, "utf8");
7322
+ const content = await readFile8(manifest, "utf8");
7063
7323
  const parsed = parseSkillFrontmatter(content);
7064
7324
  if (parsed && !seen.has(parsed.name)) {
7065
7325
  seen.add(parsed.name);
@@ -7162,7 +7422,7 @@ var mergeSkills = (repoSkills, vfsSkills, onCollision) => {
7162
7422
  var loadSkillInstructions = async (skill, engine) => {
7163
7423
  const raw = skill.source.kind === "vfs" ? decoder.decode(
7164
7424
  await requireEngine(engine).vfs.readFile(skill.source.tenantId, skill.skillPath)
7165
- ) : await readFile7(skill.skillPath, "utf8");
7425
+ ) : await readFile8(skill.skillPath, "utf8");
7166
7426
  const match = raw.match(FRONTMATTER_PATTERN3);
7167
7427
  return match ? match[2].trim() : raw.trim();
7168
7428
  };
@@ -7183,7 +7443,7 @@ var readSkillResource = async (skill, relativePath, engine) => {
7183
7443
  if (!fullPath.startsWith(skill.skillDir)) {
7184
7444
  throw new Error("Path escapes the skill directory");
7185
7445
  }
7186
- return await readFile7(fullPath, "utf8");
7446
+ return await readFile8(fullPath, "utf8");
7187
7447
  };
7188
7448
  var requireEngine = (engine) => {
7189
7449
  if (!engine) {
@@ -7311,8 +7571,8 @@ function convertSchema(schema) {
7311
7571
 
7312
7572
  // src/skill-tools.ts
7313
7573
  import { defineTool as defineTool9 } from "@poncho-ai/sdk";
7314
- import { access as access2, readdir as readdir3, stat as stat2 } from "fs/promises";
7315
- 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";
7316
7576
  import { pathToFileURL } from "url";
7317
7577
  import { createJiti as createJiti2 } from "jiti";
7318
7578
  var findSkill = async (options, tenantId, name) => {
@@ -7497,7 +7757,7 @@ var createSkillTools = (options) => {
7497
7757
  error: `Script "${resolved2.relativePath}" for skill "${name}" is not allowed by policy.`
7498
7758
  };
7499
7759
  }
7500
- await access2(resolved2.fullPath);
7760
+ await access3(resolved2.fullPath);
7501
7761
  const fn2 = await loadRunnableScriptFunction(resolved2.fullPath);
7502
7762
  const output2 = await fn2(payload, {
7503
7763
  scope: "skill",
@@ -7513,7 +7773,7 @@ var createSkillTools = (options) => {
7513
7773
  error: `Script "${resolved.relativePath}" is not allowed by policy.`
7514
7774
  };
7515
7775
  }
7516
- await access2(resolved.fullPath);
7776
+ await access3(resolved.fullPath);
7517
7777
  const fn = await loadRunnableScriptFunction(resolved.fullPath);
7518
7778
  const output = await fn(payload, {
7519
7779
  scope: "agent",
@@ -7533,7 +7793,7 @@ var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", "
7533
7793
  var VFS_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".ts", ".mts"]);
7534
7794
  var listRepoSkillScripts = async (skill, isScriptAllowed) => {
7535
7795
  const scripts = await collectScriptFiles(skill.skillDir);
7536
- 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(
7537
7797
  (relativePath) => relativePath.includes("/") ? relativePath : `./${relativePath}`
7538
7798
  ).filter((path) => isScriptAllowed ? isScriptAllowed(skill.name, path) : true).sort();
7539
7799
  };
@@ -7566,7 +7826,7 @@ var listVfsSkillScripts = async (skill, engine, isScriptAllowed) => {
7566
7826
  return found.filter((path) => isScriptAllowed ? isScriptAllowed(skill.name, path) : true).sort();
7567
7827
  };
7568
7828
  var collectScriptFiles = async (directory) => {
7569
- const entries = await readdir3(directory, { withFileTypes: true });
7829
+ const entries = await readdir4(directory, { withFileTypes: true });
7570
7830
  const files = [];
7571
7831
  for (const entry of entries) {
7572
7832
  if (entry.name === "node_modules") continue;
@@ -7575,7 +7835,7 @@ var collectScriptFiles = async (directory) => {
7575
7835
  let isFile = entry.isFile();
7576
7836
  if (entry.isSymbolicLink()) {
7577
7837
  try {
7578
- const s = await stat2(fullPath);
7838
+ const s = await stat3(fullPath);
7579
7839
  isDir = s.isDirectory();
7580
7840
  isFile = s.isFile();
7581
7841
  } catch {
@@ -7597,7 +7857,7 @@ var collectScriptFiles = async (directory) => {
7597
7857
  };
7598
7858
  var normalizeScriptPolicyPath = (relativePath) => {
7599
7859
  const trimmed = relativePath.trim();
7600
- const normalized = normalize3(trimmed).split(sep2).join("/");
7860
+ const normalized = normalize3(trimmed).split(sep3).join("/");
7601
7861
  if (normalized.startsWith("/")) {
7602
7862
  throw new Error("Script path must be relative and within the allowed directory");
7603
7863
  }
@@ -7611,7 +7871,7 @@ var resolveScriptPath = (baseDir, relativePath, containmentDir) => {
7611
7871
  const normalized = normalizeScriptPolicyPath(relativePath);
7612
7872
  const fullPath = resolve10(baseDir, normalized);
7613
7873
  const boundary = resolve10(containmentDir ?? baseDir);
7614
- if (!fullPath.startsWith(`${boundary}${sep2}`) && fullPath !== boundary) {
7874
+ if (!fullPath.startsWith(`${boundary}${sep3}`) && fullPath !== boundary) {
7615
7875
  throw new Error("Script path must stay inside the allowed directory");
7616
7876
  }
7617
7877
  const extension = extname(fullPath).toLowerCase();
@@ -8775,6 +9035,8 @@ var AgentHarness = class _AgentHarness {
8775
9035
  storageEngine;
8776
9036
  /** Bash environment manager (creates per-tenant bash instances). */
8777
9037
  bashManager;
9038
+ /** Read-only virtual mounts overlaid on the VFS. Empty by default. */
9039
+ virtualMounts = [];
8778
9040
  resolveToolAccess(toolName) {
8779
9041
  const tools = this.loadedConfig?.tools;
8780
9042
  if (!tools) return true;
@@ -8788,8 +9050,8 @@ var AgentHarness = class _AgentHarness {
8788
9050
  return true;
8789
9051
  }
8790
9052
  isToolEnabled(name) {
8791
- const access3 = this.resolveToolAccess(name);
8792
- if (access3 === false) return false;
9053
+ const access4 = this.resolveToolAccess(name);
9054
+ if (access4 === false) return false;
8793
9055
  if (name === "write_file" || name === "edit_file" || name === "delete_file" || name === "delete_directory") {
8794
9056
  return this.shouldEnableWriteTool();
8795
9057
  }
@@ -8946,6 +9208,7 @@ var AgentHarness = class _AgentHarness {
8946
9208
  this.storageEngine = options.storageEngine;
8947
9209
  this.injectedStorageEngine = true;
8948
9210
  }
9211
+ this.virtualMounts = options.virtualMounts ?? [];
8949
9212
  if (options.toolDefinitions?.length) {
8950
9213
  this.dispatcher.registerMany(options.toolDefinitions);
8951
9214
  }
@@ -9117,6 +9380,10 @@ var AgentHarness = class _AgentHarness {
9117
9380
  return this.loadedSkills;
9118
9381
  }
9119
9382
  const effectiveTenant = tenantId || "__default__";
9383
+ const engineWithRefresh = this.storageEngine;
9384
+ if (typeof engineWithRefresh.refreshPathCache === "function") {
9385
+ await engineWithRefresh.refreshPathCache(effectiveTenant);
9386
+ }
9120
9387
  const fingerprint = this.computeVfsSkillFingerprint(effectiveTenant);
9121
9388
  const cached = this.skillCache.get(effectiveTenant);
9122
9389
  if (cached && cached.fingerprint === fingerprint) {
@@ -9367,7 +9634,7 @@ var AgentHarness = class _AgentHarness {
9367
9634
  }
9368
9635
  try {
9369
9636
  const agentFilePath = resolve11(this.workingDir, "AGENT.md");
9370
- const rawContent = await readFile8(agentFilePath, "utf8");
9637
+ const rawContent = await readFile9(agentFilePath, "utf8");
9371
9638
  if (rawContent === this.agentFileFingerprint) {
9372
9639
  return false;
9373
9640
  }
@@ -9440,7 +9707,7 @@ var AgentHarness = class _AgentHarness {
9440
9707
  }
9441
9708
  } else {
9442
9709
  const agentFilePath = resolve11(this.workingDir, "AGENT.md");
9443
- const agentRawContent = await readFile8(agentFilePath, "utf8");
9710
+ const agentRawContent = await readFile9(agentFilePath, "utf8");
9444
9711
  this.parsedAgent = parseAgentMarkdown(agentRawContent);
9445
9712
  this.agentFileFingerprint = agentRawContent;
9446
9713
  const identity = await ensureAgentIdentity(this.workingDir);
@@ -9487,7 +9754,8 @@ var AgentHarness = class _AgentHarness {
9487
9754
  { maxFileSize, maxTotalStorage },
9488
9755
  bashWorkingDir,
9489
9756
  config?.bash,
9490
- config?.network
9757
+ config?.network,
9758
+ this.virtualMounts
9491
9759
  );
9492
9760
  this.registerIfMissing(createBashTool(this.bashManager));
9493
9761
  const getFs = (tenantId) => this.bashManager.getFs(tenantId);
@@ -9585,9 +9853,9 @@ var AgentHarness = class _AgentHarness {
9585
9853
  await writeFile6(filePath, json, "utf8");
9586
9854
  },
9587
9855
  async load() {
9588
- const { readFile: readFile9 } = await import("fs/promises");
9856
+ const { readFile: readFile10 } = await import("fs/promises");
9589
9857
  try {
9590
- return await readFile9(filePath, "utf8");
9858
+ return await readFile10(filePath, "utf8");
9591
9859
  } catch {
9592
9860
  return void 0;
9593
9861
  }
@@ -9606,9 +9874,9 @@ var AgentHarness = class _AgentHarness {
9606
9874
  await writeFile6(filePath, json, "utf8");
9607
9875
  },
9608
9876
  async load() {
9609
- const { readFile: readFile9 } = await import("fs/promises");
9877
+ const { readFile: readFile10 } = await import("fs/promises");
9610
9878
  try {
9611
- return await readFile9(filePath, "utf8");
9879
+ return await readFile10(filePath, "utf8");
9612
9880
  } catch {
9613
9881
  return void 0;
9614
9882
  }
@@ -9622,12 +9890,12 @@ var AgentHarness = class _AgentHarness {
9622
9890
  let browserMod;
9623
9891
  try {
9624
9892
  const { existsSync } = await import("fs");
9625
- const { join, dirname: dirname6 } = await import("path");
9893
+ const { join: join2, dirname: dirname6 } = await import("path");
9626
9894
  const { pathToFileURL: pathToFileURL2 } = await import("url");
9627
9895
  let searchDir = this.workingDir;
9628
9896
  let entryPath;
9629
9897
  for (; ; ) {
9630
- 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");
9631
9899
  if (existsSync(candidate)) {
9632
9900
  entryPath = candidate;
9633
9901
  break;
@@ -9948,7 +10216,7 @@ ${this.skillFingerprint}`;
9948
10216
  ];
9949
10217
  for (const file of input.files) {
9950
10218
  if (this.uploadStore) {
9951
- const buf = Buffer.from(file.data, "base64");
10219
+ const buf = await decodeFileInputData(file.data);
9952
10220
  const key = deriveUploadKey(buf, file.mediaType);
9953
10221
  const ref = await this.uploadStore.put(key, buf, file.mediaType);
9954
10222
  parts.push({
@@ -13063,7 +13331,7 @@ var runConversationTurn = async (opts) => {
13063
13331
  if (opts.files && opts.files.length > 0 && opts.harness.uploadStore) {
13064
13332
  const uploadedParts = await Promise.all(
13065
13333
  opts.files.map(async (f) => {
13066
- const buf = Buffer.from(f.data, "base64");
13334
+ const buf = await decodeFileInputData(f.data);
13067
13335
  const key = deriveUploadKey(buf, f.mediaType);
13068
13336
  const ref = await opts.harness.uploadStore.put(key, buf, f.mediaType);
13069
13337
  return {
@@ -13417,6 +13685,7 @@ export {
13417
13685
  createTurnDraftState,
13418
13686
  createUploadStore,
13419
13687
  createWriteTool,
13688
+ decodeFileInputData,
13420
13689
  defaultAgentDefinition,
13421
13690
  defineTool13 as defineTool,
13422
13691
  deleteOpenAICodexSession,