@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.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +10 -0
- package/dist/index.js +69 -9
- package/package.json +1 -1
- package/src/harness.ts +76 -8
- package/src/skill-tools.ts +6 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.28.
|
|
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
|
[34mCLI[39m tsup v8.5.1
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
12
|
-
[32mESM[39m ⚡️ Build success in
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m291.95 KB[39m
|
|
12
|
+
[32mESM[39m ⚡️ Build success in 123ms
|
|
13
13
|
[34mDTS[39m Build start
|
|
14
|
-
[32mDTS[39m ⚡️ Build success in
|
|
14
|
+
[32mDTS[39m ⚡️ Build success in 6599ms
|
|
15
15
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m29.62 KB[39m
|
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
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
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
|
|
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
package/src/harness.ts
CHANGED
|
@@ -812,10 +812,15 @@ export class AgentHarness {
|
|
|
812
812
|
if (!rawScript) {
|
|
813
813
|
return false;
|
|
814
814
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
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.
|
package/src/skill-tools.ts
CHANGED
|
@@ -244,7 +244,8 @@ export const createSkillTools = (
|
|
|
244
244
|
error: `Unknown skill: "${name}". Available skills: ${knownNames}`,
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
|
-
const
|
|
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("
|
|
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
|
-
|
|
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();
|