@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/dist/index.js CHANGED
@@ -7,8 +7,6 @@ import YAML from "yaml";
7
7
 
8
8
  // src/tool-policy.ts
9
9
  var MCP_PATTERN = /^[^/*\s]+\/(\*|[^/*\s]+)$/;
10
- var MCP_TOOL_PATTERN = /^(\*|[^/*\s]+)$/;
11
- var SCRIPT_PATTERN = /^[^/*\s]+\/(\*|[^*\s]+)$/;
12
10
  var validateMcpPattern = (pattern, path) => {
13
11
  if (!MCP_PATTERN.test(pattern)) {
14
12
  throw new Error(
@@ -16,19 +14,35 @@ var validateMcpPattern = (pattern, path) => {
16
14
  );
17
15
  }
18
16
  };
19
- var validateMcpToolPattern = (pattern, path) => {
20
- if (!MCP_TOOL_PATTERN.test(pattern)) {
17
+ var normalizeRelativeScriptPattern = (value, path) => {
18
+ const trimmed = value.trim();
19
+ if (trimmed.length === 0) {
20
+ throw new Error(`Invalid script pattern at ${path}: value cannot be empty.`);
21
+ }
22
+ const withoutDotPrefix = trimmed.startsWith("./") ? trimmed.slice(2) : trimmed;
23
+ const normalized = withoutDotPrefix.replaceAll("\\", "/");
24
+ if (normalized.startsWith("/") || normalized === ".." || normalized.startsWith("../")) {
21
25
  throw new Error(
22
- `Invalid MCP tool pattern at ${path}: "${pattern}". Expected "tool" or "*".`
26
+ `Invalid script pattern at ${path}: "${value}". Expected a relative path.`
23
27
  );
24
28
  }
25
- };
26
- var validateScriptPattern = (pattern, path) => {
27
- if (!SCRIPT_PATTERN.test(pattern)) {
29
+ const segments = normalized.split("/");
30
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
28
31
  throw new Error(
29
- `Invalid script pattern at ${path}: "${pattern}". Expected "skill/script-path" or "skill/*".`
32
+ `Invalid script pattern at ${path}: "${value}". Expected a normalized relative path.`
30
33
  );
31
34
  }
35
+ return `./${segments.join("/")}`;
36
+ };
37
+ var isSiblingScriptsPattern = (pattern) => pattern === "./scripts" || pattern.startsWith("./scripts/");
38
+ var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39
+ var matchesRelativeScriptPattern = (value, pattern) => {
40
+ const normalizedValue = normalizeRelativeScriptPattern(value, "value");
41
+ const normalizedPattern = normalizeRelativeScriptPattern(pattern, "pattern");
42
+ const regex = new RegExp(
43
+ `^${escapeRegex(normalizedPattern).replaceAll("\\*", ".*")}$`
44
+ );
45
+ return regex.test(normalizedValue);
32
46
  };
33
47
  var splitPattern = (pattern) => {
34
48
  const slash = pattern.indexOf("/");
@@ -48,48 +62,6 @@ var matchesSlashPattern = (value, pattern) => {
48
62
  }
49
63
  return targetName === patternName;
50
64
  };
51
- var mergePolicyForEnvironment = (policy, environment) => {
52
- const base = {
53
- mode: policy?.mode,
54
- include: [...policy?.include ?? []],
55
- exclude: [...policy?.exclude ?? []]
56
- };
57
- const env = policy?.byEnvironment?.[environment];
58
- if (!env) {
59
- return base;
60
- }
61
- return {
62
- mode: env.mode ?? base.mode,
63
- include: env.include ? [...env.include] : base.include,
64
- exclude: env.exclude ? [...env.exclude] : base.exclude
65
- };
66
- };
67
- var applyToolPolicy = (values, policy) => {
68
- const mode = policy?.mode ?? "all";
69
- const include = policy?.include ?? [];
70
- const exclude = policy?.exclude ?? [];
71
- const allowed = [];
72
- const filteredOut = [];
73
- for (const value of values) {
74
- const inInclude = include.some((pattern) => matchesSlashPattern(value, pattern));
75
- const inExclude = exclude.some((pattern) => matchesSlashPattern(value, pattern));
76
- let keep = true;
77
- if (mode === "allowlist") {
78
- keep = inInclude;
79
- } else if (mode === "denylist") {
80
- keep = !inExclude;
81
- }
82
- if (mode === "all" && exclude.length > 0) {
83
- keep = !inExclude;
84
- }
85
- if (keep) {
86
- allowed.push(value);
87
- } else {
88
- filteredOut.push(value);
89
- }
90
- }
91
- return { allowed, filteredOut };
92
- };
93
65
 
94
66
  // src/agent-parser.ts
95
67
  var FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
@@ -109,17 +81,46 @@ var parseAgentMarkdown = (content) => {
109
81
  }
110
82
  const modelValue = asRecord(parsed.model);
111
83
  const limitsValue = asRecord(parsed.limits);
112
- const allowedToolsList = Array.isArray(parsed["allowed-tools"]) ? parsed["allowed-tools"].filter((item) => typeof item === "string") : [];
113
- const mcpTools = [];
114
- const scriptTools = [];
115
- for (const [index, tool] of allowedToolsList.entries()) {
116
- if (tool.startsWith("mcp:")) {
117
- const withoutPrefix = tool.slice(4);
118
- mcpTools.push(withoutPrefix);
119
- validateMcpPattern(withoutPrefix, `AGENT.md frontmatter allowed-tools[${index}]`);
120
- } else if (tool.includes("/scripts/")) {
121
- scriptTools.push(tool);
122
- validateScriptPattern(tool, `AGENT.md frontmatter allowed-tools[${index}]`);
84
+ const parseTools = (key) => {
85
+ const entries = Array.isArray(parsed[key]) ? parsed[key].filter((item) => typeof item === "string") : [];
86
+ const mcp = [];
87
+ const scripts = [];
88
+ for (const [index, entry] of entries.entries()) {
89
+ if (entry.startsWith("mcp:")) {
90
+ const withoutPrefix = entry.slice(4);
91
+ validateMcpPattern(withoutPrefix, `AGENT.md frontmatter ${key}[${index}]`);
92
+ mcp.push(withoutPrefix);
93
+ continue;
94
+ }
95
+ scripts.push(
96
+ normalizeRelativeScriptPattern(entry, `AGENT.md frontmatter ${key}[${index}]`)
97
+ );
98
+ }
99
+ return { mcp, scripts };
100
+ };
101
+ const allowedTools = parseTools("allowed-tools");
102
+ const approvalRequired = parseTools("approval-required");
103
+ for (const pattern of approvalRequired.mcp) {
104
+ const matchesAllowed = allowedTools.mcp.some(
105
+ (allowedPattern) => matchesSlashPattern(pattern, allowedPattern)
106
+ );
107
+ if (!matchesAllowed) {
108
+ throw new Error(
109
+ `Invalid AGENT.md frontmatter approval-required: MCP pattern "${pattern}" must be included in allowed-tools.`
110
+ );
111
+ }
112
+ }
113
+ for (const pattern of approvalRequired.scripts) {
114
+ if (pattern.startsWith("./scripts/")) {
115
+ continue;
116
+ }
117
+ const matchesAllowed = allowedTools.scripts.some(
118
+ (allowedPattern) => matchesRelativeScriptPattern(pattern, allowedPattern)
119
+ );
120
+ if (!matchesAllowed) {
121
+ throw new Error(
122
+ `Invalid AGENT.md frontmatter approval-required: script pattern "${pattern}" must be included in allowed-tools when outside ./scripts/.`
123
+ );
123
124
  }
124
125
  }
125
126
  const frontmatter = {
@@ -135,9 +136,13 @@ var parseAgentMarkdown = (content) => {
135
136
  maxSteps: asNumberOrUndefined(limitsValue.maxSteps),
136
137
  timeout: asNumberOrUndefined(limitsValue.timeout)
137
138
  } : void 0,
138
- allowedTools: mcpTools.length > 0 || scriptTools.length > 0 ? {
139
- mcp: mcpTools.length > 0 ? mcpTools : void 0,
140
- scripts: scriptTools.length > 0 ? scriptTools : void 0
139
+ allowedTools: allowedTools.mcp.length > 0 || allowedTools.scripts.length > 0 ? {
140
+ mcp: allowedTools.mcp.length > 0 ? allowedTools.mcp : void 0,
141
+ scripts: allowedTools.scripts.length > 0 ? allowedTools.scripts : void 0
142
+ } : void 0,
143
+ approvalRequired: approvalRequired.mcp.length > 0 || approvalRequired.scripts.length > 0 ? {
144
+ mcp: approvalRequired.mcp.length > 0 ? approvalRequired.mcp : void 0,
145
+ scripts: approvalRequired.scripts.length > 0 ? approvalRequired.scripts : void 0
141
146
  } : void 0
142
147
  };
143
148
  return {
@@ -1058,43 +1063,8 @@ var LocalMcpBridge = class {
1058
1063
  `Invalid MCP auth config for "${name}": auth.type "bearer" requires auth.tokenEnv.`
1059
1064
  );
1060
1065
  }
1061
- this.validatePolicy(server, name);
1062
1066
  }
1063
1067
  }
1064
- validatePolicy(server, serverName) {
1065
- const policy = server.tools;
1066
- const validateList = (values, path) => {
1067
- for (const [index, value] of (values ?? []).entries()) {
1068
- validateMcpToolPattern(value, `${path}[${index}]`);
1069
- }
1070
- };
1071
- validateList(policy?.include, `mcp.${serverName}.tools.include`);
1072
- validateList(policy?.exclude, `mcp.${serverName}.tools.exclude`);
1073
- validateList(
1074
- policy?.byEnvironment?.development?.include,
1075
- `mcp.${serverName}.tools.byEnvironment.development.include`
1076
- );
1077
- validateList(
1078
- policy?.byEnvironment?.development?.exclude,
1079
- `mcp.${serverName}.tools.byEnvironment.development.exclude`
1080
- );
1081
- validateList(
1082
- policy?.byEnvironment?.staging?.include,
1083
- `mcp.${serverName}.tools.byEnvironment.staging.include`
1084
- );
1085
- validateList(
1086
- policy?.byEnvironment?.staging?.exclude,
1087
- `mcp.${serverName}.tools.byEnvironment.staging.exclude`
1088
- );
1089
- validateList(
1090
- policy?.byEnvironment?.production?.include,
1091
- `mcp.${serverName}.tools.byEnvironment.production.include`
1092
- );
1093
- validateList(
1094
- policy?.byEnvironment?.production?.exclude,
1095
- `mcp.${serverName}.tools.byEnvironment.production.exclude`
1096
- );
1097
- }
1098
1068
  getServerName(server) {
1099
1069
  return server.name ?? server.url;
1100
1070
  }
@@ -1219,7 +1189,7 @@ var LocalMcpBridge = class {
1219
1189
  }
1220
1190
  return output.sort();
1221
1191
  }
1222
- async loadTools(requestedPatterns, environment = "development") {
1192
+ async loadTools(requestedPatterns) {
1223
1193
  for (const [index, pattern] of requestedPatterns.entries()) {
1224
1194
  validateMcpPattern(pattern, `requestedPatterns[${index}]`);
1225
1195
  }
@@ -1227,7 +1197,6 @@ var LocalMcpBridge = class {
1227
1197
  if (requestedPatterns.length === 0) {
1228
1198
  return tools;
1229
1199
  }
1230
- const filteredByPolicy = [];
1231
1200
  const filteredByIntent = [];
1232
1201
  for (const server of this.remoteServers) {
1233
1202
  const serverName = this.getServerName(server);
@@ -1237,20 +1206,12 @@ var LocalMcpBridge = class {
1237
1206
  }
1238
1207
  const discovered = this.toolCatalog.get(serverName) ?? [];
1239
1208
  const fullNames = discovered.map((tool) => `${serverName}/${tool.name}`);
1240
- const effectivePolicy = mergePolicyForEnvironment(server.tools, environment);
1241
- const fullPatternPolicy = effectivePolicy ? {
1242
- ...effectivePolicy,
1243
- include: effectivePolicy.include?.map((p) => `${serverName}/${p}`),
1244
- exclude: effectivePolicy.exclude?.map((p) => `${serverName}/${p}`)
1245
- } : effectivePolicy;
1246
- const policyDecision = applyToolPolicy(fullNames, fullPatternPolicy);
1247
- filteredByPolicy.push(...policyDecision.filteredOut);
1248
- const selectedFullNames = policyDecision.allowed.filter(
1209
+ const selectedFullNames = fullNames.filter(
1249
1210
  (toolName) => requestedPatterns.some((pattern) => matchesSlashPattern(toolName, pattern))
1250
1211
  );
1251
- for (const allowedTool of policyDecision.allowed) {
1252
- if (!selectedFullNames.includes(allowedTool)) {
1253
- filteredByIntent.push(allowedTool);
1212
+ for (const discoveredTool of fullNames) {
1213
+ if (!selectedFullNames.includes(discoveredTool)) {
1214
+ filteredByIntent.push(discoveredTool);
1254
1215
  }
1255
1216
  }
1256
1217
  const selectedRawNames = new Set(
@@ -1264,7 +1225,7 @@ var LocalMcpBridge = class {
1264
1225
  this.log("info", "tools.selected", {
1265
1226
  requestedPatternCount: requestedPatterns.length,
1266
1227
  registeredCount: tools.length,
1267
- filteredByPolicyCount: filteredByPolicy.length,
1228
+ filteredByPolicyCount: 0,
1268
1229
  filteredByIntentCount: filteredByIntent.length
1269
1230
  });
1270
1231
  return tools;
@@ -1322,7 +1283,7 @@ var createModelProvider = (provider) => {
1322
1283
  };
1323
1284
 
1324
1285
  // src/skill-context.ts
1325
- import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
1286
+ import { readFile as readFile4, readdir as readdir2, stat } from "fs/promises";
1326
1287
  import { dirname as dirname3, resolve as resolve5, normalize } from "path";
1327
1288
  import YAML2 from "yaml";
1328
1289
  var DEFAULT_SKILL_DIRS = ["skills"];
@@ -1351,26 +1312,53 @@ var parseSkillFrontmatter = (content) => {
1351
1312
  return void 0;
1352
1313
  }
1353
1314
  const description = typeof parsed.description === "string" ? parsed.description.trim() : "";
1354
- const allowedToolsList = Array.isArray(parsed["allowed-tools"]) ? parsed["allowed-tools"].filter((item) => typeof item === "string") : [];
1355
- const mcpTools = [];
1356
- const scriptTools = [];
1357
- for (const [index, tool] of allowedToolsList.entries()) {
1358
- if (tool.startsWith("mcp:")) {
1359
- const withoutPrefix = tool.slice(4);
1360
- mcpTools.push(withoutPrefix);
1361
- validateMcpPattern(withoutPrefix, `SKILL.md frontmatter allowed-tools[${index}]`);
1362
- } else if (tool.includes("/scripts/")) {
1363
- scriptTools.push(tool);
1364
- validateScriptPattern(tool, `SKILL.md frontmatter allowed-tools[${index}]`);
1315
+ const parseTools = (key) => {
1316
+ const entries = Array.isArray(parsed[key]) ? parsed[key].filter((item) => typeof item === "string") : [];
1317
+ const mcp = [];
1318
+ const scripts = [];
1319
+ for (const [index, entry] of entries.entries()) {
1320
+ if (entry.startsWith("mcp:")) {
1321
+ const withoutPrefix = entry.slice(4);
1322
+ validateMcpPattern(withoutPrefix, `SKILL.md frontmatter ${key}[${index}]`);
1323
+ mcp.push(withoutPrefix);
1324
+ continue;
1325
+ }
1326
+ scripts.push(
1327
+ normalizeRelativeScriptPattern(entry, `SKILL.md frontmatter ${key}[${index}]`)
1328
+ );
1329
+ }
1330
+ return { mcp, scripts };
1331
+ };
1332
+ const allowedTools = parseTools("allowed-tools");
1333
+ const approvalRequired = parseTools("approval-required");
1334
+ for (const pattern of approvalRequired.mcp) {
1335
+ const matchesAllowed = allowedTools.mcp.some(
1336
+ (allowedPattern) => matchesSlashPattern(pattern, allowedPattern)
1337
+ );
1338
+ if (!matchesAllowed) {
1339
+ throw new Error(
1340
+ `Invalid SKILL.md frontmatter approval-required: MCP pattern "${pattern}" must be included in allowed-tools.`
1341
+ );
1342
+ }
1343
+ }
1344
+ for (const pattern of approvalRequired.scripts) {
1345
+ if (isSiblingScriptsPattern(pattern)) {
1346
+ continue;
1347
+ }
1348
+ const matchesAllowed = allowedTools.scripts.some(
1349
+ (allowedPattern) => matchesRelativeScriptPattern(pattern, allowedPattern)
1350
+ );
1351
+ if (!matchesAllowed) {
1352
+ throw new Error(
1353
+ `Invalid SKILL.md frontmatter approval-required: script pattern "${pattern}" must be included in allowed-tools when outside ./scripts/.`
1354
+ );
1365
1355
  }
1366
1356
  }
1367
1357
  return {
1368
1358
  name,
1369
1359
  description,
1370
- allowedTools: {
1371
- mcp: mcpTools,
1372
- scripts: scriptTools
1373
- }
1360
+ allowedTools,
1361
+ approvalRequired
1374
1362
  };
1375
1363
  };
1376
1364
  var collectSkillManifests = async (directory) => {
@@ -1378,11 +1366,22 @@ var collectSkillManifests = async (directory) => {
1378
1366
  const files = [];
1379
1367
  for (const entry of entries) {
1380
1368
  const fullPath = resolve5(directory, entry.name);
1381
- if (entry.isDirectory()) {
1369
+ let isDir = entry.isDirectory();
1370
+ let isFile = entry.isFile();
1371
+ if (entry.isSymbolicLink()) {
1372
+ try {
1373
+ const s = await stat(fullPath);
1374
+ isDir = s.isDirectory();
1375
+ isFile = s.isFile();
1376
+ } catch {
1377
+ continue;
1378
+ }
1379
+ }
1380
+ if (isDir) {
1382
1381
  files.push(...await collectSkillManifests(fullPath));
1383
1382
  continue;
1384
1383
  }
1385
- if (entry.isFile() && entry.name.toLowerCase() === "skill.md") {
1384
+ if (isFile && entry.name.toLowerCase() === "skill.md") {
1386
1385
  files.push(fullPath);
1387
1386
  }
1388
1387
  }
@@ -1416,6 +1415,9 @@ var loadSkillMetadata = async (workingDir, extraSkillPaths) => {
1416
1415
  if (message.startsWith("Invalid MCP tool pattern") || message.startsWith("Invalid script pattern")) {
1417
1416
  throw new Error(`Invalid SKILL.md frontmatter at ${manifest}: ${message}`);
1418
1417
  }
1418
+ if (message.startsWith("Invalid SKILL.md frontmatter approval-required")) {
1419
+ throw new Error(`Invalid SKILL.md frontmatter at ${manifest}: ${message}`);
1420
+ }
1419
1421
  }
1420
1422
  }
1421
1423
  return skills;
@@ -1546,16 +1548,13 @@ function convertSchema(schema) {
1546
1548
 
1547
1549
  // src/skill-tools.ts
1548
1550
  import { defineTool as defineTool3 } from "@poncho-ai/sdk";
1549
- import { access as access2, readdir as readdir3 } from "fs/promises";
1551
+ import { access as access2, readdir as readdir3, stat as stat2 } from "fs/promises";
1550
1552
  import { extname, normalize as normalize2, resolve as resolve6, sep as sep2 } from "path";
1551
1553
  import { pathToFileURL } from "url";
1552
1554
  import { createJiti } from "jiti";
1553
1555
  var createSkillTools = (skills, options) => {
1554
- if (skills.length === 0) {
1555
- return [];
1556
- }
1557
1556
  const skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
1558
- const knownNames = skills.map((skill) => skill.name).join(", ");
1557
+ const knownNames = skills.length > 0 ? skills.map((skill) => skill.name).join(", ") : "(none)";
1559
1558
  return [
1560
1559
  defineTool3({
1561
1560
  name: "activate_skill",
@@ -1677,7 +1676,7 @@ var createSkillTools = (skills, options) => {
1677
1676
  }),
1678
1677
  defineTool3({
1679
1678
  name: "list_skill_scripts",
1680
- description: `List JavaScript/TypeScript script files available under a skill's scripts directory. Available skills: ${knownNames}`,
1679
+ description: `List JavaScript/TypeScript script files available under a skill directory (recursive). Available skills: ${knownNames}`,
1681
1680
  inputSchema: {
1682
1681
  type: "object",
1683
1682
  properties: {
@@ -1712,62 +1711,81 @@ var createSkillTools = (skills, options) => {
1712
1711
  }),
1713
1712
  defineTool3({
1714
1713
  name: "run_skill_script",
1715
- description: `Run a JavaScript/TypeScript module in a skill's scripts directory. Uses default export function or named run/main/handler function. Available skills: ${knownNames}`,
1714
+ description: `Run a JavaScript/TypeScript module in a skill or project directory. Uses default export function or named run/main/handler function. Available skills: ${knownNames}`,
1716
1715
  inputSchema: {
1717
1716
  type: "object",
1718
1717
  properties: {
1719
1718
  skill: {
1720
1719
  type: "string",
1721
- description: "Name of the skill"
1720
+ description: "Optional skill name. Omit to run a project-level script relative to AGENT.md directory."
1722
1721
  },
1723
1722
  script: {
1724
1723
  type: "string",
1725
- description: "Relative path under scripts/ (e.g. scripts/summarize.ts or summarize.ts)"
1724
+ description: "Relative script path from the skill/project root (e.g. ./fetch-page.ts, scripts/summarize.ts, tools/multiply.ts)"
1726
1725
  },
1727
1726
  input: {
1728
1727
  type: "object",
1729
1728
  description: "Optional JSON input payload passed to the script function"
1730
1729
  }
1731
1730
  },
1732
- required: ["skill", "script"],
1731
+ required: ["script"],
1733
1732
  additionalProperties: false
1734
1733
  },
1735
1734
  handler: async (input) => {
1736
1735
  const name = typeof input.skill === "string" ? input.skill.trim() : "";
1737
1736
  const script = typeof input.script === "string" ? input.script.trim() : "";
1738
1737
  const payload = typeof input.input === "object" && input.input !== null ? input.input : {};
1739
- const skill = skillsByName.get(name);
1740
- if (!skill) {
1741
- return {
1742
- error: `Unknown skill: "${name}". Available skills: ${knownNames}`
1743
- };
1744
- }
1745
1738
  if (!script) {
1746
1739
  return { error: "Script path is required" };
1747
1740
  }
1748
1741
  try {
1749
- const scriptPath = resolveSkillScriptPath(skill, script);
1750
- const relativeScript = `scripts/${scriptPath.slice(resolve6(skill.skillDir, "scripts").length + 1).split(sep2).join("/")}`;
1751
- if (options?.isScriptAllowed && !options.isScriptAllowed(name, relativeScript)) {
1742
+ if (name) {
1743
+ const skill = skillsByName.get(name);
1744
+ if (!skill) {
1745
+ return {
1746
+ error: `Unknown skill: "${name}". Available skills: ${knownNames}`
1747
+ };
1748
+ }
1749
+ const resolved2 = resolveScriptPath(skill.skillDir, script);
1750
+ if (options?.isScriptAllowed && !options.isScriptAllowed(name, resolved2.relativePath)) {
1751
+ return {
1752
+ error: `Script "${resolved2.relativePath}" for skill "${name}" is not allowed by policy.`
1753
+ };
1754
+ }
1755
+ await access2(resolved2.fullPath);
1756
+ const fn2 = await loadRunnableScriptFunction(resolved2.fullPath);
1757
+ const output2 = await fn2(payload, {
1758
+ scope: "skill",
1759
+ skill: name,
1760
+ scriptPath: resolved2.fullPath
1761
+ });
1752
1762
  return {
1753
- error: `Script "${relativeScript}" for skill "${name}" is not allowed by policy.`
1763
+ skill: name,
1764
+ script: resolved2.relativePath,
1765
+ output: output2
1754
1766
  };
1755
1767
  }
1756
- await access2(scriptPath);
1757
- const fn = await loadRunnableScriptFunction(scriptPath);
1768
+ const baseDir = options?.workingDir ?? process.cwd();
1769
+ const resolved = resolveScriptPath(baseDir, script);
1770
+ if (options?.isRootScriptAllowed && !options.isRootScriptAllowed(resolved.relativePath)) {
1771
+ return {
1772
+ error: `Script "${resolved.relativePath}" is not allowed by policy.`
1773
+ };
1774
+ }
1775
+ await access2(resolved.fullPath);
1776
+ const fn = await loadRunnableScriptFunction(resolved.fullPath);
1758
1777
  const output = await fn(payload, {
1759
- skill: name,
1760
- skillDir: skill.skillDir,
1761
- scriptPath
1778
+ scope: "agent",
1779
+ scriptPath: resolved.fullPath
1762
1780
  });
1763
1781
  return {
1764
- skill: name,
1765
- script,
1782
+ skill: null,
1783
+ script: resolved.relativePath,
1766
1784
  output
1767
1785
  };
1768
1786
  } catch (err) {
1769
1787
  return {
1770
- error: `Failed to run script "${script}" in skill "${name}": ${err instanceof Error ? err.message : String(err)}`
1788
+ error: name ? `Failed to run script "${script}" in skill "${name}": ${err instanceof Error ? err.message : String(err)}` : `Failed to run script "${script}" from AGENT scope: ${err instanceof Error ? err.message : String(err)}`
1771
1789
  };
1772
1790
  }
1773
1791
  }
@@ -1776,25 +1794,35 @@ var createSkillTools = (skills, options) => {
1776
1794
  };
1777
1795
  var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]);
1778
1796
  var listSkillScripts = async (skill, isScriptAllowed) => {
1779
- const scriptsRoot = resolve6(skill.skillDir, "scripts");
1780
- try {
1781
- await access2(scriptsRoot);
1782
- } catch {
1783
- return [];
1784
- }
1785
- const scripts = await collectScriptFiles(scriptsRoot);
1786
- return scripts.map((fullPath) => `scripts/${fullPath.slice(scriptsRoot.length + 1).split(sep2).join("/")}`).filter((path) => isScriptAllowed ? isScriptAllowed(skill.name, path) : true).sort();
1797
+ const scripts = await collectScriptFiles(skill.skillDir);
1798
+ return scripts.map((fullPath) => fullPath.slice(skill.skillDir.length + 1).split(sep2).join("/")).filter((relativePath) => relativePath.toLowerCase() !== "skill.md").map(
1799
+ (relativePath) => relativePath.includes("/") ? relativePath : `./${relativePath}`
1800
+ ).filter((path) => isScriptAllowed ? isScriptAllowed(skill.name, path) : true).sort();
1787
1801
  };
1788
1802
  var collectScriptFiles = async (directory) => {
1789
1803
  const entries = await readdir3(directory, { withFileTypes: true });
1790
1804
  const files = [];
1791
1805
  for (const entry of entries) {
1806
+ if (entry.name === "node_modules") {
1807
+ continue;
1808
+ }
1792
1809
  const fullPath = resolve6(directory, entry.name);
1793
- if (entry.isDirectory()) {
1810
+ let isDir = entry.isDirectory();
1811
+ let isFile = entry.isFile();
1812
+ if (entry.isSymbolicLink()) {
1813
+ try {
1814
+ const s = await stat2(fullPath);
1815
+ isDir = s.isDirectory();
1816
+ isFile = s.isFile();
1817
+ } catch {
1818
+ continue;
1819
+ }
1820
+ }
1821
+ if (isDir) {
1794
1822
  files.push(...await collectScriptFiles(fullPath));
1795
1823
  continue;
1796
1824
  }
1797
- if (entry.isFile()) {
1825
+ if (isFile) {
1798
1826
  const extension = extname(fullPath).toLowerCase();
1799
1827
  if (SCRIPT_EXTENSIONS.has(extension)) {
1800
1828
  files.push(fullPath);
@@ -1803,16 +1831,23 @@ var collectScriptFiles = async (directory) => {
1803
1831
  }
1804
1832
  return files;
1805
1833
  };
1806
- var resolveSkillScriptPath = (skill, relativePath) => {
1807
- const normalized = normalize2(relativePath);
1834
+ var normalizeScriptPolicyPath = (relativePath) => {
1835
+ const trimmed = relativePath.trim();
1836
+ const normalized = normalize2(trimmed).split(sep2).join("/");
1808
1837
  if (normalized.startsWith("..") || normalized.startsWith("/")) {
1809
- throw new Error("Script path must be relative and within the skill directory");
1838
+ throw new Error("Script path must be relative and within the allowed directory");
1839
+ }
1840
+ const withoutDotPrefix = normalized.startsWith("./") ? normalized.slice(2) : normalized;
1841
+ if (withoutDotPrefix.length === 0 || withoutDotPrefix === ".") {
1842
+ throw new Error("Script path must point to a file");
1810
1843
  }
1811
- const normalizedWithPrefix = normalized.startsWith("scripts/") ? normalized : `scripts/${normalized}`;
1812
- const fullPath = resolve6(skill.skillDir, normalizedWithPrefix);
1813
- const scriptsRoot = resolve6(skill.skillDir, "scripts");
1814
- if (!fullPath.startsWith(`${scriptsRoot}${sep2}`) && fullPath !== scriptsRoot) {
1815
- throw new Error("Script path must stay inside the scripts directory");
1844
+ return withoutDotPrefix;
1845
+ };
1846
+ var resolveScriptPath = (baseDir, relativePath) => {
1847
+ const normalized = normalizeScriptPolicyPath(relativePath);
1848
+ const fullPath = resolve6(baseDir, normalized);
1849
+ if (!fullPath.startsWith(`${resolve6(baseDir)}${sep2}`) && fullPath !== resolve6(baseDir)) {
1850
+ throw new Error("Script path must stay inside the allowed directory");
1816
1851
  }
1817
1852
  const extension = extname(fullPath).toLowerCase();
1818
1853
  if (!SCRIPT_EXTENSIONS.has(extension)) {
@@ -1820,7 +1855,10 @@ var resolveSkillScriptPath = (skill, relativePath) => {
1820
1855
  `Unsupported script extension "${extension || "(none)"}". Allowed: ${[...SCRIPT_EXTENSIONS].join(", ")}`
1821
1856
  );
1822
1857
  }
1823
- return fullPath;
1858
+ return {
1859
+ fullPath,
1860
+ relativePath: `./${normalized}`
1861
+ };
1824
1862
  };
1825
1863
  var loadRunnableScriptFunction = async (scriptPath) => {
1826
1864
  const loaded = await loadScriptModule(scriptPath);
@@ -1979,26 +2017,38 @@ You are running locally in development mode. Treat this as an editable agent wor
1979
2017
 
1980
2018
  You can extend your own capabilities by creating custom JavaScript/TypeScript scripts:
1981
2019
 
1982
- - Create scripts under \`skills/<skill-name>/scripts/\` to add new functionality
2020
+ - Create scripts under \`skills/<skill-name>/\` (recursive) to add new functionality
1983
2021
  - Scripts can perform any Node.js operations: API calls, file processing, data transformations, web scraping, etc.
1984
2022
  - Use the \`run_skill_script\` tool to execute these scripts and integrate results into your workflow
1985
2023
  - This allows you to dynamically add custom tools and capabilities as users need them, without requiring external dependencies or MCP servers
1986
2024
 
2025
+ ## Skill Authoring Guardrails
2026
+
2027
+ - Every \`SKILL.md\` must include YAML frontmatter between \`---\` markers.
2028
+ - Required frontmatter fields for discovery: \`name\` (non-empty string). Add \`description\` whenever possible.
2029
+ - \`allowed-tools\` and \`approval-required\` belong in SKILL frontmatter (not in script files).
2030
+ - MCP entries in frontmatter must use \`mcp:server/tool\` or \`mcp:server/*\`.
2031
+ - Script entries in frontmatter must be relative paths (for example \`./scripts/fetch.ts\`, \`./tools/audit.ts\`, \`./fetch-page.ts\`).
2032
+ - \`approval-required\` should be a stricter subset of allowed access:
2033
+ - MCP entries must also appear in \`allowed-tools\`.
2034
+ - Script entries outside \`./scripts/\` must also appear in \`allowed-tools\`.
2035
+ - Keep MCP server connection details (\`url\`, auth env vars) in \`poncho.config.js\` only.
2036
+
1987
2037
  ## When users ask about customization:
1988
2038
 
1989
2039
  - Explain and edit \`poncho.config.js\` for model/provider, storage+memory, auth, telemetry, and MCP settings.
1990
2040
  - Help create or update local skills under \`skills/<skill-name>/SKILL.md\`.
1991
- - For executable skills, add JavaScript/TypeScript scripts under \`skills/<skill-name>/scripts/\` and run them via \`run_skill_script\`.
1992
- - For MCP setup, default to direct \`poncho.config.js\` edits (\`mcp\` entries with URL, bearer token env, and tool policy).
1993
- - Keep MCP server connection details in \`poncho.config.js\` only (name/url/auth/tools policy). Do not move server definitions into \`SKILL.md\`.
1994
- - In \`AGENT.md\`/\`SKILL.md\` frontmatter, declare MCP tools in \`allowed-tools\` array as \`mcp:server/pattern\` (for example \`mcp:linear/*\` or \`mcp:linear/list_issues\`).
2041
+ - For executable scripts, use a sibling \`scripts/\` directory next to \`AGENT.md\` or \`SKILL.md\`; run via \`run_skill_script\`.
2042
+ - To use a custom script folder (for example \`tools/\`), declare it in \`allowed-tools\` and gate sensitive paths with \`approval-required\` in frontmatter.
2043
+ - For MCP setup, default to direct \`poncho.config.js\` edits (\`mcp\` entries with URL and bearer token env).
2044
+ - Keep MCP server connection details in \`poncho.config.js\` only (name/url/auth). Do not move server definitions into \`SKILL.md\`.
2045
+ - In \`AGENT.md\`/\`SKILL.md\` frontmatter, declare MCP tools in \`allowed-tools\` array as \`mcp:server/pattern\` (for example \`mcp:linear/*\` or \`mcp:linear/list_issues\`), and use \`approval-required\` for human-gated calls.
1995
2046
  - Never use nested MCP objects in skill frontmatter (for example \`mcp: [{ name, url, auth }]\`).
1996
- - To scope tools to a skill: keep server config in \`poncho.config.js\`, add desired \`allowed-tools\` patterns in that skill's \`SKILL.md\`, and remove global \`AGENT.md\` patterns if you do not want global availability.
2047
+ - To scope tools to a skill: keep server config in \`poncho.config.js\`, add desired \`allowed-tools\`/ \`approval-required\` patterns in that skill's \`SKILL.md\`, and remove global \`AGENT.md\` patterns if you do not want global availability.
1997
2048
  - Do not invent unsupported top-level config keys (for example \`model\` in \`poncho.config.js\`). Keep existing config structure unless README/spec explicitly says otherwise.
1998
- - In \`poncho.config.js\`, MCP tool patterns are scoped within each server object, so use just the tool name (for example \`include: ["*"]\` or \`include: ["list_issues"]\`), not the full \`server/tool\` format.
1999
2049
  - Keep \`poncho.config.js\` valid JavaScript and preserve existing imports/types/comments. If there is a JSDoc type import, do not rewrite it to a different package name.
2000
2050
  - Preferred MCP config shape in \`poncho.config.js\`:
2001
- \`mcp: [{ name: "linear", url: "https://mcp.linear.app/mcp", auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" }, tools: { mode: "allowlist", include: ["*"] } }]\`
2051
+ \`mcp: [{ name: "linear", url: "https://mcp.linear.app/mcp", auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" } }]\`
2002
2052
  - If shell/CLI access exists, you can use \`poncho mcp add --url ... --name ... --auth-bearer-env ...\`, then \`poncho mcp tools list <server>\` and \`poncho mcp tools select <server>\`.
2003
2053
  - If shell/CLI access is unavailable, ask the user to run needed commands and provide exact copy-paste commands.
2004
2054
  - For setup, skills, MCP, auth, storage, telemetry, or "how do I..." questions, proactively read \`README.md\` with \`read_file\` before answering.
@@ -2076,9 +2126,6 @@ var AgentHarness = class {
2076
2126
  this.dispatcher.registerMany(options.toolDefinitions);
2077
2127
  }
2078
2128
  }
2079
- runtimeEnvironment() {
2080
- return this.environment ?? "development";
2081
- }
2082
2129
  listActiveSkills() {
2083
2130
  return [...this.activeSkillNames].sort();
2084
2131
  }
@@ -2088,6 +2135,12 @@ var AgentHarness = class {
2088
2135
  getAgentScriptIntent() {
2089
2136
  return this.parsedAgent?.frontmatter.allowedTools?.scripts ?? [];
2090
2137
  }
2138
+ getAgentMcpApprovalPatterns() {
2139
+ return this.parsedAgent?.frontmatter.approvalRequired?.mcp ?? [];
2140
+ }
2141
+ getAgentScriptApprovalPatterns() {
2142
+ return this.parsedAgent?.frontmatter.approvalRequired?.scripts ?? [];
2143
+ }
2091
2144
  getRequestedMcpPatterns() {
2092
2145
  const skillPatterns = /* @__PURE__ */ new Set();
2093
2146
  for (const skillName of this.activeSkillNames) {
@@ -2105,34 +2158,96 @@ var AgentHarness = class {
2105
2158
  return this.getAgentMcpIntent();
2106
2159
  }
2107
2160
  getRequestedScriptPatterns() {
2108
- const skillPatterns = /* @__PURE__ */ new Set();
2161
+ const patterns = new Set(this.getAgentScriptIntent());
2109
2162
  for (const skillName of this.activeSkillNames) {
2110
2163
  const skill = this.loadedSkills.find((entry) => entry.name === skillName);
2111
2164
  if (!skill) {
2112
2165
  continue;
2113
2166
  }
2114
2167
  for (const pattern of skill.allowedTools.scripts) {
2168
+ patterns.add(pattern);
2169
+ }
2170
+ }
2171
+ return [...patterns];
2172
+ }
2173
+ getRequestedMcpApprovalPatterns() {
2174
+ const skillPatterns = /* @__PURE__ */ new Set();
2175
+ for (const skillName of this.activeSkillNames) {
2176
+ const skill = this.loadedSkills.find((entry) => entry.name === skillName);
2177
+ if (!skill) {
2178
+ continue;
2179
+ }
2180
+ for (const pattern of skill.approvalRequired.mcp) {
2115
2181
  skillPatterns.add(pattern);
2116
2182
  }
2117
2183
  }
2118
2184
  if (skillPatterns.size > 0) {
2119
2185
  return [...skillPatterns];
2120
2186
  }
2121
- return this.getAgentScriptIntent();
2187
+ return this.getAgentMcpApprovalPatterns();
2188
+ }
2189
+ getRequestedScriptApprovalPatterns() {
2190
+ const patterns = new Set(this.getAgentScriptApprovalPatterns());
2191
+ for (const skillName of this.activeSkillNames) {
2192
+ const skill = this.loadedSkills.find((entry) => entry.name === skillName);
2193
+ if (!skill) {
2194
+ continue;
2195
+ }
2196
+ for (const pattern of skill.approvalRequired.scripts) {
2197
+ patterns.add(pattern);
2198
+ }
2199
+ }
2200
+ return [...patterns];
2122
2201
  }
2123
2202
  isScriptAllowedByPolicy(skill, scriptPath) {
2124
- const identifier = `${skill}/${scriptPath}`;
2125
- const intentPatterns = this.getRequestedScriptPatterns();
2126
- const matchedIntent = intentPatterns.length === 0 ? true : intentPatterns.some((pattern) => matchesSlashPattern(identifier, pattern));
2127
- if (!matchedIntent) {
2128
- return false;
2203
+ const normalizedScriptPath = normalizeRelativeScriptPattern(
2204
+ scriptPath,
2205
+ "run_skill_script input.script"
2206
+ );
2207
+ const isSkillRootScript = normalizedScriptPath.startsWith("./") && !normalizedScriptPath.slice(2).includes("/");
2208
+ if (isSiblingScriptsPattern(normalizedScriptPath) || isSkillRootScript) {
2209
+ return true;
2210
+ }
2211
+ const skillPatterns = this.loadedSkills.find((entry) => entry.name === skill)?.allowedTools.scripts ?? [];
2212
+ const intentPatterns = /* @__PURE__ */ new Set([
2213
+ ...this.getAgentScriptIntent(),
2214
+ ...skillPatterns,
2215
+ ...this.getRequestedScriptPatterns()
2216
+ ]);
2217
+ return [...intentPatterns].some(
2218
+ (pattern) => matchesRelativeScriptPattern(normalizedScriptPath, pattern)
2219
+ );
2220
+ }
2221
+ isRootScriptAllowedByPolicy(scriptPath) {
2222
+ const normalizedScriptPath = normalizeRelativeScriptPattern(
2223
+ scriptPath,
2224
+ "run_skill_script input.script"
2225
+ );
2226
+ if (isSiblingScriptsPattern(normalizedScriptPath)) {
2227
+ return true;
2129
2228
  }
2130
- const policy = mergePolicyForEnvironment(
2131
- this.loadedConfig?.scripts,
2132
- this.runtimeEnvironment()
2229
+ const patterns = this.getAgentScriptIntent();
2230
+ return patterns.some(
2231
+ (pattern) => matchesRelativeScriptPattern(normalizedScriptPath, pattern)
2133
2232
  );
2134
- const decision = applyToolPolicy([identifier], policy);
2135
- return decision.allowed.length > 0;
2233
+ }
2234
+ requiresApprovalForToolCall(toolName, input) {
2235
+ if (toolName === "run_skill_script") {
2236
+ const rawScript = typeof input.script === "string" ? input.script.trim() : "";
2237
+ if (!rawScript) {
2238
+ return false;
2239
+ }
2240
+ const canonicalPath = normalizeRelativeScriptPattern(
2241
+ `./${normalizeScriptPolicyPath(rawScript)}`,
2242
+ "run_skill_script input.script"
2243
+ );
2244
+ const scriptPatterns = this.getRequestedScriptApprovalPatterns();
2245
+ return scriptPatterns.some(
2246
+ (pattern) => matchesRelativeScriptPattern(canonicalPath, pattern)
2247
+ );
2248
+ }
2249
+ const mcpPatterns = this.getRequestedMcpApprovalPatterns();
2250
+ return mcpPatterns.some((pattern) => matchesSlashPattern(toolName, pattern));
2136
2251
  }
2137
2252
  async refreshMcpTools(reason) {
2138
2253
  if (!this.mcpBridge) {
@@ -2147,10 +2262,7 @@ var AgentHarness = class {
2147
2262
  );
2148
2263
  return;
2149
2264
  }
2150
- const tools = await this.mcpBridge.loadTools(
2151
- requestedPatterns,
2152
- this.runtimeEnvironment()
2153
- );
2265
+ const tools = await this.mcpBridge.loadTools(requestedPatterns);
2154
2266
  this.dispatcher.registerMany(tools);
2155
2267
  for (const tool of tools) {
2156
2268
  this.registeredMcpToolNames.add(tool.name);
@@ -2165,43 +2277,9 @@ var AgentHarness = class {
2165
2277
  })}`
2166
2278
  );
2167
2279
  }
2168
- validateScriptPolicyConfig(config) {
2169
- const check = (values, path) => {
2170
- for (const [index, value] of (values ?? []).entries()) {
2171
- validateScriptPattern(value, `${path}[${index}]`);
2172
- }
2173
- };
2174
- check(config?.scripts?.include, "poncho.config.js scripts.include");
2175
- check(config?.scripts?.exclude, "poncho.config.js scripts.exclude");
2176
- check(
2177
- config?.scripts?.byEnvironment?.development?.include,
2178
- "poncho.config.js scripts.byEnvironment.development.include"
2179
- );
2180
- check(
2181
- config?.scripts?.byEnvironment?.development?.exclude,
2182
- "poncho.config.js scripts.byEnvironment.development.exclude"
2183
- );
2184
- check(
2185
- config?.scripts?.byEnvironment?.staging?.include,
2186
- "poncho.config.js scripts.byEnvironment.staging.include"
2187
- );
2188
- check(
2189
- config?.scripts?.byEnvironment?.staging?.exclude,
2190
- "poncho.config.js scripts.byEnvironment.staging.exclude"
2191
- );
2192
- check(
2193
- config?.scripts?.byEnvironment?.production?.include,
2194
- "poncho.config.js scripts.byEnvironment.production.include"
2195
- );
2196
- check(
2197
- config?.scripts?.byEnvironment?.production?.exclude,
2198
- "poncho.config.js scripts.byEnvironment.production.exclude"
2199
- );
2200
- }
2201
2280
  async initialize() {
2202
2281
  this.parsedAgent = await parseAgentFile(this.workingDir);
2203
2282
  const config = await loadPonchoConfig(this.workingDir);
2204
- this.validateScriptPolicyConfig(config);
2205
2283
  this.loadedConfig = config;
2206
2284
  this.registerConfiguredBuiltInTools(config);
2207
2285
  const provider = this.parsedAgent.frontmatter.model?.provider ?? "anthropic";
@@ -2228,7 +2306,9 @@ var AgentHarness = class {
2228
2306
  return this.listActiveSkills();
2229
2307
  },
2230
2308
  onListActiveSkills: () => this.listActiveSkills(),
2231
- isScriptAllowed: (skill, scriptPath) => this.isScriptAllowedByPolicy(skill, scriptPath)
2309
+ isScriptAllowed: (skill, scriptPath) => this.isScriptAllowedByPolicy(skill, scriptPath),
2310
+ isRootScriptAllowed: (scriptPath) => this.isRootScriptAllowedByPolicy(scriptPath),
2311
+ workingDir: this.workingDir
2232
2312
  })
2233
2313
  );
2234
2314
  if (memoryConfig?.enabled) {
@@ -2454,7 +2534,7 @@ ${boundedMainMemory.trim()}` : "";
2454
2534
  });
2455
2535
  const modelName = agent.frontmatter.model?.name ?? "claude-opus-4-5";
2456
2536
  const temperature = agent.frontmatter.model?.temperature ?? 0.2;
2457
- const maxTokens = agent.frontmatter.model?.maxTokens ?? 1024;
2537
+ const maxTokens = agent.frontmatter.model?.maxTokens;
2458
2538
  const telemetryEnabled = this.loadedConfig?.telemetry?.enabled !== false;
2459
2539
  const latitudeApiKey = this.loadedConfig?.telemetry?.latitude?.apiKey;
2460
2540
  const result = await streamText({
@@ -2463,7 +2543,7 @@ ${boundedMainMemory.trim()}` : "";
2463
2543
  messages: coreMessages,
2464
2544
  tools,
2465
2545
  temperature,
2466
- maxTokens,
2546
+ ...typeof maxTokens === "number" ? { maxTokens } : {},
2467
2547
  experimental_telemetry: {
2468
2548
  isEnabled: telemetryEnabled && !!latitudeApiKey
2469
2549
  }
@@ -2526,8 +2606,11 @@ ${boundedMainMemory.trim()}` : "";
2526
2606
  for (const call of toolCalls) {
2527
2607
  const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
2528
2608
  yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
2529
- const definition = this.dispatcher.get(runtimeToolName);
2530
- if (definition?.requiresApproval) {
2609
+ const requiresApproval = this.requiresApprovalForToolCall(
2610
+ runtimeToolName,
2611
+ call.input
2612
+ );
2613
+ if (requiresApproval) {
2531
2614
  const approvalId = `approval_${randomUUID2()}`;
2532
2615
  yield pushEvent({
2533
2616
  type: "tool:approval:required",
@@ -3597,6 +3680,7 @@ export {
3597
3680
  loadSkillContext,
3598
3681
  loadSkillInstructions,
3599
3682
  loadSkillMetadata,
3683
+ normalizeScriptPolicyPath,
3600
3684
  parseAgentFile,
3601
3685
  parseAgentMarkdown,
3602
3686
  readSkillResource,