@poncho-ai/harness 0.7.0 → 0.7.2
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 +11 -11
- package/CHANGELOG.md +6 -0
- package/dist/index.d.ts +25 -20
- package/dist/index.js +330 -246
- package/package.json +3 -3
- package/src/agent-parser.ts +70 -23
- package/src/config.ts +0 -2
- package/src/harness.ts +127 -75
- package/src/mcp.ts +5 -58
- package/src/skill-context.ts +86 -27
- package/src/skill-tools.ts +103 -60
- package/src/state.ts +6 -0
- package/src/tool-policy.ts +39 -0
- package/test/agent-parser.test.ts +26 -0
- package/test/harness.test.ts +113 -26
- package/test/mcp.test.ts +9 -24
package/test/harness.test.ts
CHANGED
|
@@ -235,6 +235,11 @@ description: Simple math scripts
|
|
|
235
235
|
"export async function run() { return { ok: true }; }\n",
|
|
236
236
|
"utf8",
|
|
237
237
|
);
|
|
238
|
+
await writeFile(
|
|
239
|
+
join(dir, "skills", "math", "fetch-page.ts"),
|
|
240
|
+
"export default async function run() { return { ok: true, root: true }; }\n",
|
|
241
|
+
"utf8",
|
|
242
|
+
);
|
|
238
243
|
await writeFile(
|
|
239
244
|
join(dir, "skills", "math", "scripts", "README.md"),
|
|
240
245
|
"# not executable\n",
|
|
@@ -249,7 +254,7 @@ description: Simple math scripts
|
|
|
249
254
|
const result = await listScripts!.handler({ skill: "math" });
|
|
250
255
|
expect(result).toEqual({
|
|
251
256
|
skill: "math",
|
|
252
|
-
scripts: ["scripts/add.ts", "scripts/nested/multiply.js"],
|
|
257
|
+
scripts: ["./fetch-page.ts", "scripts/add.ts", "scripts/nested/multiply.js"],
|
|
253
258
|
});
|
|
254
259
|
});
|
|
255
260
|
|
|
@@ -287,6 +292,14 @@ description: Simple math scripts
|
|
|
287
292
|
const b = Number(input?.b ?? 0);
|
|
288
293
|
return { sum: a + b };
|
|
289
294
|
}
|
|
295
|
+
`,
|
|
296
|
+
"utf8",
|
|
297
|
+
);
|
|
298
|
+
await writeFile(
|
|
299
|
+
join(dir, "skills", "math", "fetch-page.ts"),
|
|
300
|
+
`export default async function run() {
|
|
301
|
+
return { kind: "root-script" };
|
|
302
|
+
}
|
|
290
303
|
`,
|
|
291
304
|
"utf8",
|
|
292
305
|
);
|
|
@@ -298,14 +311,56 @@ description: Simple math scripts
|
|
|
298
311
|
expect(runner).toBeDefined();
|
|
299
312
|
const result = await runner!.handler({
|
|
300
313
|
skill: "math",
|
|
301
|
-
script: "add.ts",
|
|
314
|
+
script: "scripts/add.ts",
|
|
302
315
|
input: { a: 2, b: 3 },
|
|
303
316
|
});
|
|
304
317
|
expect(result).toEqual({
|
|
305
318
|
skill: "math",
|
|
306
|
-
script: "add.ts",
|
|
319
|
+
script: "./scripts/add.ts",
|
|
307
320
|
output: { sum: 5 },
|
|
308
321
|
});
|
|
322
|
+
const rootResult = await runner!.handler({
|
|
323
|
+
skill: "math",
|
|
324
|
+
script: "./fetch-page.ts",
|
|
325
|
+
});
|
|
326
|
+
expect(rootResult).toEqual({
|
|
327
|
+
skill: "math",
|
|
328
|
+
script: "./fetch-page.ts",
|
|
329
|
+
output: { kind: "root-script" },
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("runs AGENT-scope scripts from root scripts directory", async () => {
|
|
334
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-harness-root-script-run-"));
|
|
335
|
+
await mkdir(join(dir, "scripts"), { recursive: true });
|
|
336
|
+
await writeFile(
|
|
337
|
+
join(dir, "scripts", "ping.ts"),
|
|
338
|
+
"export default async function run() { return { pong: true }; }\n",
|
|
339
|
+
"utf8",
|
|
340
|
+
);
|
|
341
|
+
await writeFile(
|
|
342
|
+
join(dir, "AGENT.md"),
|
|
343
|
+
`---
|
|
344
|
+
name: root-run-agent
|
|
345
|
+
model:
|
|
346
|
+
provider: anthropic
|
|
347
|
+
name: claude-opus-4-5
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
# Root Run Agent
|
|
351
|
+
`,
|
|
352
|
+
"utf8",
|
|
353
|
+
);
|
|
354
|
+
const harness = new AgentHarness({ workingDir: dir });
|
|
355
|
+
await harness.initialize();
|
|
356
|
+
const runner = harness.listTools().find((tool) => tool.name === "run_skill_script");
|
|
357
|
+
expect(runner).toBeDefined();
|
|
358
|
+
const result = await runner!.handler({ script: "scripts/ping.ts" });
|
|
359
|
+
expect(result).toEqual({
|
|
360
|
+
skill: null,
|
|
361
|
+
script: "./scripts/ping.ts",
|
|
362
|
+
output: { pong: true },
|
|
363
|
+
});
|
|
309
364
|
});
|
|
310
365
|
|
|
311
366
|
it("blocks path traversal in run_skill_script", async () => {
|
|
@@ -345,12 +400,12 @@ description: Safe skill
|
|
|
345
400
|
script: "../outside.ts",
|
|
346
401
|
});
|
|
347
402
|
expect(result).toMatchObject({
|
|
348
|
-
error: expect.stringContaining("must be relative and within the
|
|
403
|
+
error: expect.stringContaining("must be relative and within the allowed directory"),
|
|
349
404
|
});
|
|
350
405
|
});
|
|
351
406
|
|
|
352
|
-
it("
|
|
353
|
-
const dir = await mkdtemp(join(tmpdir(), "poncho-harness-script-
|
|
407
|
+
it("requires allowed-tools entries for non-standard script directories", async () => {
|
|
408
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-harness-script-allowed-tools-"));
|
|
354
409
|
await writeFile(
|
|
355
410
|
join(dir, "AGENT.md"),
|
|
356
411
|
`---
|
|
@@ -361,26 +416,18 @@ model:
|
|
|
361
416
|
---
|
|
362
417
|
|
|
363
418
|
# Script Policy Agent
|
|
364
|
-
`,
|
|
365
|
-
"utf8",
|
|
366
|
-
);
|
|
367
|
-
await writeFile(
|
|
368
|
-
join(dir, "poncho.config.js"),
|
|
369
|
-
`export default {
|
|
370
|
-
scripts: {
|
|
371
|
-
mode: "denylist",
|
|
372
|
-
exclude: ["math/scripts/add.ts"]
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
419
|
`,
|
|
376
420
|
"utf8",
|
|
377
421
|
);
|
|
378
422
|
await mkdir(join(dir, "skills", "math", "scripts"), { recursive: true });
|
|
423
|
+
await mkdir(join(dir, "skills", "math", "tools"), { recursive: true });
|
|
379
424
|
await writeFile(
|
|
380
425
|
join(dir, "skills", "math", "SKILL.md"),
|
|
381
426
|
`---
|
|
382
427
|
name: math
|
|
383
428
|
description: Math scripts
|
|
429
|
+
allowed-tools:
|
|
430
|
+
- ./tools/multiply.ts
|
|
384
431
|
---
|
|
385
432
|
|
|
386
433
|
# Math
|
|
@@ -392,6 +439,11 @@ description: Math scripts
|
|
|
392
439
|
"export default async function run() { return { ok: true }; }\n",
|
|
393
440
|
"utf8",
|
|
394
441
|
);
|
|
442
|
+
await writeFile(
|
|
443
|
+
join(dir, "skills", "math", "tools", "multiply.ts"),
|
|
444
|
+
"export default async function run() { return { ok: true, kind: 'tools' }; }\n",
|
|
445
|
+
"utf8",
|
|
446
|
+
);
|
|
395
447
|
const harness = new AgentHarness({ workingDir: dir });
|
|
396
448
|
await harness.initialize();
|
|
397
449
|
const listScripts = harness.listTools().find((tool) => tool.name === "list_skill_scripts");
|
|
@@ -399,11 +451,17 @@ description: Math scripts
|
|
|
399
451
|
expect(listScripts).toBeDefined();
|
|
400
452
|
expect(runScript).toBeDefined();
|
|
401
453
|
const listed = await listScripts!.handler({ skill: "math" });
|
|
402
|
-
expect(listed).toEqual({
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
454
|
+
expect(listed).toEqual({
|
|
455
|
+
skill: "math",
|
|
456
|
+
scripts: ["scripts/add.ts", "tools/multiply.ts"],
|
|
457
|
+
});
|
|
458
|
+
const result = await runScript!.handler({ skill: "math", script: "scripts/add.ts" });
|
|
459
|
+
expect(result).toMatchObject({ output: { ok: true } });
|
|
460
|
+
const toolsResult = await runScript!.handler({
|
|
461
|
+
skill: "math",
|
|
462
|
+
script: "./tools/multiply.ts",
|
|
406
463
|
});
|
|
464
|
+
expect(toolsResult).toMatchObject({ output: { ok: true, kind: "tools" } });
|
|
407
465
|
});
|
|
408
466
|
|
|
409
467
|
|
|
@@ -431,6 +489,38 @@ allowed-tools:
|
|
|
431
489
|
expect(metadata[0]?.name).toBe("summarize");
|
|
432
490
|
expect(metadata[0]?.allowedTools.mcp).toEqual(["linear/list_issues", "linear/get_issue"]);
|
|
433
491
|
expect(metadata[0]?.allowedTools.scripts).toEqual([]);
|
|
492
|
+
expect(metadata[0]?.approvalRequired.mcp).toEqual([]);
|
|
493
|
+
expect(metadata[0]?.approvalRequired.scripts).toEqual([]);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("parses approval-required patterns from SKILL.md frontmatter", async () => {
|
|
497
|
+
const dir = await mkdtemp(join(tmpdir(), "poncho-harness-approval-required-"));
|
|
498
|
+
await mkdir(join(dir, "skills", "triage"), { recursive: true });
|
|
499
|
+
await writeFile(
|
|
500
|
+
join(dir, "skills", "triage", "SKILL.md"),
|
|
501
|
+
`---
|
|
502
|
+
name: triage
|
|
503
|
+
description: Triage
|
|
504
|
+
allowed-tools:
|
|
505
|
+
- mcp:github/list_issues
|
|
506
|
+
- mcp:github/create_issue
|
|
507
|
+
- ./tools/open-pr.ts
|
|
508
|
+
approval-required:
|
|
509
|
+
- mcp:github/create_issue
|
|
510
|
+
- ./scripts/review.ts
|
|
511
|
+
- ./tools/open-pr.ts
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
# Triage
|
|
515
|
+
`,
|
|
516
|
+
"utf8",
|
|
517
|
+
);
|
|
518
|
+
const metadata = await loadSkillMetadata(dir);
|
|
519
|
+
expect(metadata[0]?.approvalRequired.mcp).toEqual(["github/create_issue"]);
|
|
520
|
+
expect(metadata[0]?.approvalRequired.scripts).toEqual([
|
|
521
|
+
"./scripts/review.ts",
|
|
522
|
+
"./tools/open-pr.ts",
|
|
523
|
+
]);
|
|
434
524
|
});
|
|
435
525
|
|
|
436
526
|
it("fails when SKILL.md includes invalid non-slash tool patterns", async () => {
|
|
@@ -542,8 +632,7 @@ model:
|
|
|
542
632
|
{
|
|
543
633
|
name: "remote",
|
|
544
634
|
url: "http://127.0.0.1:${address.port}/mcp",
|
|
545
|
-
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }
|
|
546
|
-
tools: { mode: "allowlist", include: ["remote/*"] }
|
|
635
|
+
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }
|
|
547
636
|
}
|
|
548
637
|
]
|
|
549
638
|
};
|
|
@@ -676,8 +765,7 @@ model:
|
|
|
676
765
|
{
|
|
677
766
|
name: "remote",
|
|
678
767
|
url: "http://127.0.0.1:${address.port}/mcp",
|
|
679
|
-
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }
|
|
680
|
-
tools: { mode: "allowlist", include: ["remote/*"] }
|
|
768
|
+
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }
|
|
681
769
|
}
|
|
682
770
|
]
|
|
683
771
|
};
|
|
@@ -712,5 +800,4 @@ allowed-tools:
|
|
|
712
800
|
await new Promise<void>((resolveClose) => mcpServer.close(() => resolveClose()));
|
|
713
801
|
});
|
|
714
802
|
|
|
715
|
-
|
|
716
803
|
});
|
package/test/mcp.test.ts
CHANGED
|
@@ -138,7 +138,7 @@ describe("mcp bridge protocol transports", () => {
|
|
|
138
138
|
).toThrow(/Duplicate MCP server name/);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
it("
|
|
141
|
+
it("selects discovered tools by requested patterns", async () => {
|
|
142
142
|
process.env.LINEAR_TOKEN = "token-123";
|
|
143
143
|
const server = createServer(async (req, res) => {
|
|
144
144
|
if (req.method === "DELETE") {
|
|
@@ -195,37 +195,22 @@ describe("mcp bridge protocol transports", () => {
|
|
|
195
195
|
if (!address || typeof address === "string") {
|
|
196
196
|
throw new Error("Unexpected server address");
|
|
197
197
|
}
|
|
198
|
-
const
|
|
199
|
-
mcp: [
|
|
200
|
-
{
|
|
201
|
-
name: "remote",
|
|
202
|
-
url: `http://127.0.0.1:${address.port}/mcp`,
|
|
203
|
-
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" },
|
|
204
|
-
tools: { mode: "allowlist", include: ["remote/a"] },
|
|
205
|
-
},
|
|
206
|
-
],
|
|
207
|
-
});
|
|
208
|
-
await allowBridge.startLocalServers();
|
|
209
|
-
await allowBridge.discoverTools();
|
|
210
|
-
const allowTools = await allowBridge.loadTools(["remote/*"]);
|
|
211
|
-
expect(allowTools.map((tool) => tool.name)).toEqual(["remote/a"]);
|
|
212
|
-
await allowBridge.stopLocalServers();
|
|
213
|
-
|
|
214
|
-
const denyBridge = new LocalMcpBridge({
|
|
198
|
+
const bridge = new LocalMcpBridge({
|
|
215
199
|
mcp: [
|
|
216
200
|
{
|
|
217
201
|
name: "remote",
|
|
218
202
|
url: `http://127.0.0.1:${address.port}/mcp`,
|
|
219
203
|
auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" },
|
|
220
|
-
tools: { mode: "denylist", exclude: ["remote/b"] },
|
|
221
204
|
},
|
|
222
205
|
],
|
|
223
206
|
});
|
|
224
|
-
await
|
|
225
|
-
await
|
|
226
|
-
const
|
|
227
|
-
expect(
|
|
228
|
-
await
|
|
207
|
+
await bridge.startLocalServers();
|
|
208
|
+
await bridge.discoverTools();
|
|
209
|
+
const exact = await bridge.loadTools(["remote/a"]);
|
|
210
|
+
expect(exact.map((tool) => tool.name)).toEqual(["remote/a"]);
|
|
211
|
+
const wildcard = await bridge.loadTools(["remote/*"]);
|
|
212
|
+
expect(wildcard.map((tool) => tool.name).sort()).toEqual(["remote/a", "remote/b"]);
|
|
213
|
+
await bridge.stopLocalServers();
|
|
229
214
|
await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
|
|
230
215
|
});
|
|
231
216
|
|