@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.
@@ -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 skill directory"),
403
+ error: expect.stringContaining("must be relative and within the allowed directory"),
349
404
  });
350
405
  });
351
406
 
352
- it("enforces scripts denylist policy from config", async () => {
353
- const dir = await mkdtemp(join(tmpdir(), "poncho-harness-script-policy-"));
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({ skill: "math", scripts: [] });
403
- const result = await runScript!.handler({ skill: "math", script: "add.ts" });
404
- expect(result).toMatchObject({
405
- error: expect.stringContaining("is not allowed by policy"),
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("applies allowlist and denylist policy filters", async () => {
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 allowBridge = new LocalMcpBridge({
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 denyBridge.startLocalServers();
225
- await denyBridge.discoverTools();
226
- const denyTools = await denyBridge.loadTools(["remote/*"]);
227
- expect(denyTools.map((tool) => tool.name).sort()).toEqual(["remote/a"]);
228
- await denyBridge.stopLocalServers();
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