@poncho-ai/harness 0.28.2 → 0.28.3

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.28.2 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.28.3 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
4
4
 
5
5
  [embed-docs] Generated poncho-docs.ts with 4 topics
@@ -8,8 +8,8 @@
8
8
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
- ESM dist/index.js 289.62 KB
12
- ESM ⚡️ Build success in 213ms
11
+ ESM dist/index.js 291.95 KB
12
+ ESM ⚡️ Build success in 123ms
13
13
  DTS Build start
14
- DTS ⚡️ Build success in 7196ms
14
+ DTS ⚡️ Build success in 6599ms
15
15
  DTS dist/index.d.ts 29.62 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.28.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`87f844b`](https://github.com/cesr/poncho-ai/commit/87f844b0a76ece87e4bba78eaf73392f857cdef2) Thanks [@cesr](https://github.com/cesr)! - Fix tool execution blowing past serverless timeout and cross-skill script paths
8
+ - Race tool batch execution against remaining soft deadline so parallel tools can't push past the hard platform timeout
9
+ - Add post-tool-execution soft deadline checkpoint for tools that finish just past the deadline
10
+ - Allow skill scripts to reference sibling directories (e.g. ../scripts/current-date.ts)
11
+ - Catch script path normalization errors in approval check instead of crashing the run
12
+
3
13
  ## 0.28.2
4
14
 
5
15
  ### Patch Changes
package/dist/index.js CHANGED
@@ -4087,7 +4087,8 @@ var createSkillTools = (skills, options) => {
4087
4087
  error: `Unknown skill: "${name}". Available skills: ${knownNames}`
4088
4088
  };
4089
4089
  }
4090
- const resolved2 = resolveScriptPath(skill.skillDir, script);
4090
+ const projectRoot = options?.workingDir ?? process.cwd();
4091
+ const resolved2 = resolveScriptPath(skill.skillDir, script, projectRoot);
4091
4092
  if (options?.isScriptAllowed && !options.isScriptAllowed(name, resolved2.relativePath)) {
4092
4093
  return {
4093
4094
  error: `Script "${resolved2.relativePath}" for skill "${name}" is not allowed by policy.`
@@ -4175,7 +4176,7 @@ var collectScriptFiles = async (directory) => {
4175
4176
  var normalizeScriptPolicyPath = (relativePath) => {
4176
4177
  const trimmed = relativePath.trim();
4177
4178
  const normalized = normalize2(trimmed).split(sep2).join("/");
4178
- if (normalized.startsWith("..") || normalized.startsWith("/")) {
4179
+ if (normalized.startsWith("/")) {
4179
4180
  throw new Error("Script path must be relative and within the allowed directory");
4180
4181
  }
4181
4182
  const withoutDotPrefix = normalized.startsWith("./") ? normalized.slice(2) : normalized;
@@ -4184,10 +4185,11 @@ var normalizeScriptPolicyPath = (relativePath) => {
4184
4185
  }
4185
4186
  return withoutDotPrefix;
4186
4187
  };
4187
- var resolveScriptPath = (baseDir, relativePath) => {
4188
+ var resolveScriptPath = (baseDir, relativePath, containmentDir) => {
4188
4189
  const normalized = normalizeScriptPolicyPath(relativePath);
4189
4190
  const fullPath = resolve9(baseDir, normalized);
4190
- if (!fullPath.startsWith(`${resolve9(baseDir)}${sep2}`) && fullPath !== resolve9(baseDir)) {
4191
+ const boundary = resolve9(containmentDir ?? baseDir);
4192
+ if (!fullPath.startsWith(`${boundary}${sep2}`) && fullPath !== boundary) {
4191
4193
  throw new Error("Script path must stay inside the allowed directory");
4192
4194
  }
4193
4195
  const extension = extname(fullPath).toLowerCase();
@@ -5325,10 +5327,15 @@ var AgentHarness = class _AgentHarness {
5325
5327
  if (!rawScript) {
5326
5328
  return false;
5327
5329
  }
5328
- const canonicalPath = normalizeRelativeScriptPattern(
5329
- `./${normalizeScriptPolicyPath(rawScript)}`,
5330
- "run_skill_script input.script"
5331
- );
5330
+ let canonicalPath;
5331
+ try {
5332
+ canonicalPath = normalizeRelativeScriptPattern(
5333
+ `./${normalizeScriptPolicyPath(rawScript)}`,
5334
+ "run_skill_script input.script"
5335
+ );
5336
+ } catch {
5337
+ return true;
5338
+ }
5332
5339
  const scriptPatterns = this.getRequestedScriptApprovalPatterns();
5333
5340
  return scriptPatterns.some(
5334
5341
  (pattern) => matchesRelativeScriptPattern(canonicalPath, pattern)
@@ -6549,7 +6556,44 @@ ${textContent}` };
6549
6556
  );
6550
6557
  }
6551
6558
  }
6552
- const batchResults = approvedCalls.length > 0 ? await this.dispatcher.executeBatch(approvedCalls, toolContext) : [];
6559
+ const TOOL_DEADLINE_SENTINEL = /* @__PURE__ */ Symbol("tool_deadline");
6560
+ const toolDeadlineRemainingMs = softDeadlineMs > 0 ? softDeadlineMs - (now() - start) : Infinity;
6561
+ let batchResults;
6562
+ if (approvedCalls.length === 0) {
6563
+ batchResults = [];
6564
+ } else if (toolDeadlineRemainingMs <= 0) {
6565
+ batchResults = TOOL_DEADLINE_SENTINEL;
6566
+ } else if (toolDeadlineRemainingMs < Infinity) {
6567
+ const raced = await Promise.race([
6568
+ this.dispatcher.executeBatch(approvedCalls, toolContext),
6569
+ new Promise(
6570
+ (resolve12) => setTimeout(() => resolve12(TOOL_DEADLINE_SENTINEL), toolDeadlineRemainingMs)
6571
+ )
6572
+ ]);
6573
+ if (raced === TOOL_DEADLINE_SENTINEL) {
6574
+ batchResults = TOOL_DEADLINE_SENTINEL;
6575
+ } else {
6576
+ batchResults = raced;
6577
+ }
6578
+ } else {
6579
+ batchResults = await this.dispatcher.executeBatch(approvedCalls, toolContext);
6580
+ }
6581
+ if (batchResults === TOOL_DEADLINE_SENTINEL) {
6582
+ const result_ = {
6583
+ status: "completed",
6584
+ response: responseText + fullText,
6585
+ steps: step,
6586
+ tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
6587
+ duration: now() - start,
6588
+ continuation: true,
6589
+ continuationMessages: [...messages],
6590
+ maxSteps,
6591
+ contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
6592
+ contextWindow
6593
+ };
6594
+ yield pushEvent({ type: "run:completed", runId, result: result_ });
6595
+ return;
6596
+ }
6553
6597
  if (isCancelled()) {
6554
6598
  yield emitCancellation();
6555
6599
  return;
@@ -6637,6 +6681,22 @@ ${textContent}` };
6637
6681
  content: JSON.stringify(toolResultsForModel),
6638
6682
  metadata: toolMsgMeta
6639
6683
  });
6684
+ if (softDeadlineMs > 0 && now() - start > softDeadlineMs) {
6685
+ const result_ = {
6686
+ status: "completed",
6687
+ response: responseText + fullText,
6688
+ steps: step,
6689
+ tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
6690
+ duration: now() - start,
6691
+ continuation: true,
6692
+ continuationMessages: [...messages],
6693
+ maxSteps,
6694
+ contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
6695
+ contextWindow
6696
+ };
6697
+ yield pushEvent({ type: "run:completed", runId, result: result_ });
6698
+ return;
6699
+ }
6640
6700
  if (this.environment === "development") {
6641
6701
  const agentChanged = await this.refreshAgentIfChanged();
6642
6702
  const skillsChanged = await this.refreshSkillsIfChanged(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.28.2",
3
+ "version": "0.28.3",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
package/src/harness.ts CHANGED
@@ -812,10 +812,15 @@ export class AgentHarness {
812
812
  if (!rawScript) {
813
813
  return false;
814
814
  }
815
- const canonicalPath = normalizeRelativeScriptPattern(
816
- `./${normalizeScriptPolicyPath(rawScript)}`,
817
- "run_skill_script input.script",
818
- );
815
+ let canonicalPath: string;
816
+ try {
817
+ canonicalPath = normalizeRelativeScriptPattern(
818
+ `./${normalizeScriptPolicyPath(rawScript)}`,
819
+ "run_skill_script input.script",
820
+ );
821
+ } catch {
822
+ return true;
823
+ }
819
824
  const scriptPatterns = this.getRequestedScriptApprovalPatterns();
820
825
  return scriptPatterns.some((pattern) =>
821
826
  matchesRelativeScriptPattern(canonicalPath, pattern),
@@ -2285,10 +2290,53 @@ ${boundedMainMemory.trim()}`
2285
2290
  }
2286
2291
  }
2287
2292
 
2288
- const batchResults =
2289
- approvedCalls.length > 0
2290
- ? await this.dispatcher.executeBatch(approvedCalls, toolContext)
2291
- : [];
2293
+ // Race tool execution against the soft deadline so long-running tool
2294
+ // batches (e.g. 4 parallel web_search calls) can't push us past the
2295
+ // hard platform timeout. If the deadline fires first, we checkpoint
2296
+ // with the pre-tool messages and the step will be re-done on
2297
+ // continuation (assistant + tool results are not yet in `messages`).
2298
+ const TOOL_DEADLINE_SENTINEL = Symbol("tool_deadline");
2299
+ const toolDeadlineRemainingMs = softDeadlineMs > 0
2300
+ ? softDeadlineMs - (now() - start)
2301
+ : Infinity;
2302
+
2303
+ let batchResults: Awaited<ReturnType<typeof this.dispatcher.executeBatch>>;
2304
+ if (approvedCalls.length === 0) {
2305
+ batchResults = [];
2306
+ } else if (toolDeadlineRemainingMs <= 0) {
2307
+ batchResults = TOOL_DEADLINE_SENTINEL as never;
2308
+ } else if (toolDeadlineRemainingMs < Infinity) {
2309
+ const raced = await Promise.race([
2310
+ this.dispatcher.executeBatch(approvedCalls, toolContext),
2311
+ new Promise<typeof TOOL_DEADLINE_SENTINEL>((resolve) =>
2312
+ setTimeout(() => resolve(TOOL_DEADLINE_SENTINEL), toolDeadlineRemainingMs),
2313
+ ),
2314
+ ]);
2315
+ if (raced === TOOL_DEADLINE_SENTINEL) {
2316
+ batchResults = TOOL_DEADLINE_SENTINEL as never;
2317
+ } else {
2318
+ batchResults = raced;
2319
+ }
2320
+ } else {
2321
+ batchResults = await this.dispatcher.executeBatch(approvedCalls, toolContext);
2322
+ }
2323
+
2324
+ if ((batchResults as unknown) === TOOL_DEADLINE_SENTINEL) {
2325
+ const result_: RunResult = {
2326
+ status: "completed",
2327
+ response: responseText + fullText,
2328
+ steps: step,
2329
+ tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
2330
+ duration: now() - start,
2331
+ continuation: true,
2332
+ continuationMessages: [...messages],
2333
+ maxSteps,
2334
+ contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
2335
+ contextWindow,
2336
+ };
2337
+ yield pushEvent({ type: "run:completed", runId, result: result_ });
2338
+ return;
2339
+ }
2292
2340
 
2293
2341
  if (isCancelled()) {
2294
2342
  yield emitCancellation();
@@ -2386,6 +2434,26 @@ ${boundedMainMemory.trim()}`
2386
2434
  metadata: toolMsgMeta as Message["metadata"],
2387
2435
  });
2388
2436
 
2437
+ // Post-tool-execution soft deadline: long-running tool batches (e.g.
2438
+ // multiple web_search calls) can push past the deadline. Checkpoint
2439
+ // now so the platform doesn't hard-kill us before we can continue.
2440
+ if (softDeadlineMs > 0 && now() - start > softDeadlineMs) {
2441
+ const result_: RunResult = {
2442
+ status: "completed",
2443
+ response: responseText + fullText,
2444
+ steps: step,
2445
+ tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
2446
+ duration: now() - start,
2447
+ continuation: true,
2448
+ continuationMessages: [...messages],
2449
+ maxSteps,
2450
+ contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
2451
+ contextWindow,
2452
+ };
2453
+ yield pushEvent({ type: "run:completed", runId, result: result_ });
2454
+ return;
2455
+ }
2456
+
2389
2457
  // In development, re-read AGENT.md and re-scan skills after tool
2390
2458
  // execution so changes are available on the next step without
2391
2459
  // requiring a server restart.
@@ -244,7 +244,8 @@ export const createSkillTools = (
244
244
  error: `Unknown skill: "${name}". Available skills: ${knownNames}`,
245
245
  };
246
246
  }
247
- const resolved = resolveScriptPath(skill.skillDir, script);
247
+ const projectRoot = options?.workingDir ?? process.cwd();
248
+ const resolved = resolveScriptPath(skill.skillDir, script, projectRoot);
248
249
  if (
249
250
  options?.isScriptAllowed &&
250
251
  !options.isScriptAllowed(name, resolved.relativePath)
@@ -357,7 +358,7 @@ const collectScriptFiles = async (directory: string): Promise<string[]> => {
357
358
  export const normalizeScriptPolicyPath = (relativePath: string): string => {
358
359
  const trimmed = relativePath.trim();
359
360
  const normalized = normalize(trimmed).split(sep).join("/");
360
- if (normalized.startsWith("..") || normalized.startsWith("/")) {
361
+ if (normalized.startsWith("/")) {
361
362
  throw new Error("Script path must be relative and within the allowed directory");
362
363
  }
363
364
  const withoutDotPrefix = normalized.startsWith("./") ? normalized.slice(2) : normalized;
@@ -370,10 +371,12 @@ export const normalizeScriptPolicyPath = (relativePath: string): string => {
370
371
  const resolveScriptPath = (
371
372
  baseDir: string,
372
373
  relativePath: string,
374
+ containmentDir?: string,
373
375
  ): { fullPath: string; relativePath: string } => {
374
376
  const normalized = normalizeScriptPolicyPath(relativePath);
375
377
  const fullPath = resolve(baseDir, normalized);
376
- if (!fullPath.startsWith(`${resolve(baseDir)}${sep}`) && fullPath !== resolve(baseDir)) {
378
+ const boundary = resolve(containmentDir ?? baseDir);
379
+ if (!fullPath.startsWith(`${boundary}${sep}`) && fullPath !== boundary) {
377
380
  throw new Error("Script path must stay inside the allowed directory");
378
381
  }
379
382
  const extension = extname(fullPath).toLowerCase();