@poncho-ai/harness 0.28.1 → 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.1 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 288.74 KB
12
- ESM ⚡️ Build success in 135ms
11
+ ESM dist/index.js 291.95 KB
12
+ ESM ⚡️ Build success in 123ms
13
13
  DTS Build start
14
- DTS ⚡️ Build success in 6964ms
14
+ DTS ⚡️ Build success in 6599ms
15
15
  DTS dist/index.d.ts 29.62 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
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
+
13
+ ## 0.28.2
14
+
15
+ ### Patch Changes
16
+
17
+ - [`98df42f`](https://github.com/cesr/poncho-ai/commit/98df42f79e0a376d0a864598557758bfa644039d) Thanks [@cesr](https://github.com/cesr)! - Fix serverless subagent and continuation reliability
18
+ - Use stable internal secret across serverless instances for callback auth
19
+ - Wrap continuation self-fetches in waitUntil to survive function shutdown
20
+ - Set runStatus during callback re-runs so clients detect active processing
21
+ - Add post-streaming soft deadline check to catch long model responses
22
+ - Client auto-recovers from abrupt stream termination and orphaned continuations
23
+ - Fix callback continuation losing \_continuationMessages when no pending results
24
+
3
25
  ## 0.28.1
4
26
 
5
27
  ### Patch Changes
package/dist/index.js CHANGED
@@ -1604,6 +1604,8 @@ Remote storage keys are namespaced and versioned, for example \`poncho:v1:<agent
1604
1604
  | \`ANTHROPIC_API_KEY\` | Yes* | Claude API key |
1605
1605
  | \`OPENAI_API_KEY\` | No | OpenAI API key (if using OpenAI) |
1606
1606
  | \`PONCHO_AUTH_TOKEN\` | No | Unified auth token (Web UI passphrase + API Bearer token) |
1607
+ | \`PONCHO_INTERNAL_SECRET\` | No | Shared secret used by internal serverless callbacks (recommended for Vercel/Lambda) |
1608
+ | \`PONCHO_SELF_BASE_URL\` | No | Explicit base URL for internal self-callbacks when auto-detection is unavailable |
1607
1609
  | \`OTEL_EXPORTER_OTLP_ENDPOINT\` | No | Telemetry destination |
1608
1610
  | \`LATITUDE_API_KEY\` | No | Latitude dashboard integration |
1609
1611
  | \`LATITUDE_PROJECT_ID\` | No | Latitude project identifier for capture traces |
@@ -4085,7 +4087,8 @@ var createSkillTools = (skills, options) => {
4085
4087
  error: `Unknown skill: "${name}". Available skills: ${knownNames}`
4086
4088
  };
4087
4089
  }
4088
- const resolved2 = resolveScriptPath(skill.skillDir, script);
4090
+ const projectRoot = options?.workingDir ?? process.cwd();
4091
+ const resolved2 = resolveScriptPath(skill.skillDir, script, projectRoot);
4089
4092
  if (options?.isScriptAllowed && !options.isScriptAllowed(name, resolved2.relativePath)) {
4090
4093
  return {
4091
4094
  error: `Script "${resolved2.relativePath}" for skill "${name}" is not allowed by policy.`
@@ -4173,7 +4176,7 @@ var collectScriptFiles = async (directory) => {
4173
4176
  var normalizeScriptPolicyPath = (relativePath) => {
4174
4177
  const trimmed = relativePath.trim();
4175
4178
  const normalized = normalize2(trimmed).split(sep2).join("/");
4176
- if (normalized.startsWith("..") || normalized.startsWith("/")) {
4179
+ if (normalized.startsWith("/")) {
4177
4180
  throw new Error("Script path must be relative and within the allowed directory");
4178
4181
  }
4179
4182
  const withoutDotPrefix = normalized.startsWith("./") ? normalized.slice(2) : normalized;
@@ -4182,10 +4185,11 @@ var normalizeScriptPolicyPath = (relativePath) => {
4182
4185
  }
4183
4186
  return withoutDotPrefix;
4184
4187
  };
4185
- var resolveScriptPath = (baseDir, relativePath) => {
4188
+ var resolveScriptPath = (baseDir, relativePath, containmentDir) => {
4186
4189
  const normalized = normalizeScriptPolicyPath(relativePath);
4187
4190
  const fullPath = resolve9(baseDir, normalized);
4188
- if (!fullPath.startsWith(`${resolve9(baseDir)}${sep2}`) && fullPath !== resolve9(baseDir)) {
4191
+ const boundary = resolve9(containmentDir ?? baseDir);
4192
+ if (!fullPath.startsWith(`${boundary}${sep2}`) && fullPath !== boundary) {
4189
4193
  throw new Error("Script path must stay inside the allowed directory");
4190
4194
  }
4191
4195
  const extension = extname(fullPath).toLowerCase();
@@ -5323,10 +5327,15 @@ var AgentHarness = class _AgentHarness {
5323
5327
  if (!rawScript) {
5324
5328
  return false;
5325
5329
  }
5326
- const canonicalPath = normalizeRelativeScriptPattern(
5327
- `./${normalizeScriptPolicyPath(rawScript)}`,
5328
- "run_skill_script input.script"
5329
- );
5330
+ let canonicalPath;
5331
+ try {
5332
+ canonicalPath = normalizeRelativeScriptPattern(
5333
+ `./${normalizeScriptPolicyPath(rawScript)}`,
5334
+ "run_skill_script input.script"
5335
+ );
5336
+ } catch {
5337
+ return true;
5338
+ }
5330
5339
  const scriptPatterns = this.getRequestedScriptApprovalPatterns();
5331
5340
  return scriptPatterns.some(
5332
5341
  (pattern) => matchesRelativeScriptPattern(canonicalPath, pattern)
@@ -6347,6 +6356,22 @@ ${textContent}` };
6347
6356
  yield emitCancellation();
6348
6357
  return;
6349
6358
  }
6359
+ if (softDeadlineMs > 0 && now() - start > softDeadlineMs) {
6360
+ const result_ = {
6361
+ status: "completed",
6362
+ response: responseText + fullText,
6363
+ steps: step,
6364
+ tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
6365
+ duration: now() - start,
6366
+ continuation: true,
6367
+ continuationMessages: [...messages],
6368
+ maxSteps,
6369
+ contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
6370
+ contextWindow
6371
+ };
6372
+ yield pushEvent({ type: "run:completed", runId, result: result_ });
6373
+ return;
6374
+ }
6350
6375
  const finishReason = await result.finishReason;
6351
6376
  if (finishReason === "error") {
6352
6377
  yield pushEvent({
@@ -6531,7 +6556,44 @@ ${textContent}` };
6531
6556
  );
6532
6557
  }
6533
6558
  }
6534
- 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
+ }
6535
6597
  if (isCancelled()) {
6536
6598
  yield emitCancellation();
6537
6599
  return;
@@ -6619,6 +6681,22 @@ ${textContent}` };
6619
6681
  content: JSON.stringify(toolResultsForModel),
6620
6682
  metadata: toolMsgMeta
6621
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
+ }
6622
6700
  if (this.environment === "development") {
6623
6701
  const agentChanged = await this.refreshAgentIfChanged();
6624
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.1",
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),
@@ -2030,6 +2035,25 @@ ${boundedMainMemory.trim()}`
2030
2035
  return;
2031
2036
  }
2032
2037
 
2038
+ // Post-streaming soft deadline: if the model stream took long enough to
2039
+ // push past the soft deadline, checkpoint now before tool execution.
2040
+ if (softDeadlineMs > 0 && now() - start > softDeadlineMs) {
2041
+ const result_: RunResult = {
2042
+ status: "completed",
2043
+ response: responseText + fullText,
2044
+ steps: step,
2045
+ tokens: { input: totalInputTokens, output: totalOutputTokens, cached: totalCachedTokens },
2046
+ duration: now() - start,
2047
+ continuation: true,
2048
+ continuationMessages: [...messages],
2049
+ maxSteps,
2050
+ contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
2051
+ contextWindow,
2052
+ };
2053
+ yield pushEvent({ type: "run:completed", runId, result: result_ });
2054
+ return;
2055
+ }
2056
+
2033
2057
  // Check finish reason for error / abnormal completions.
2034
2058
  const finishReason = await result.finishReason;
2035
2059
 
@@ -2266,10 +2290,53 @@ ${boundedMainMemory.trim()}`
2266
2290
  }
2267
2291
  }
2268
2292
 
2269
- const batchResults =
2270
- approvedCalls.length > 0
2271
- ? await this.dispatcher.executeBatch(approvedCalls, toolContext)
2272
- : [];
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
+ }
2273
2340
 
2274
2341
  if (isCancelled()) {
2275
2342
  yield emitCancellation();
@@ -2367,6 +2434,26 @@ ${boundedMainMemory.trim()}`
2367
2434
  metadata: toolMsgMeta as Message["metadata"],
2368
2435
  });
2369
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
+
2370
2457
  // In development, re-read AGENT.md and re-scan skills after tool
2371
2458
  // execution so changes are available on the next step without
2372
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();