Install-time injection of a disallowedTools deny-list into Claude copies of read-only verifier/auditor agents (mirrors the #443 effort injection); source agents stay runtime-neutral so Gemini/Qwen/Hermes are unaffected. Closes #767.
This commit is contained in:
@@ -1209,6 +1209,52 @@ function injectEffortFrontmatter(content, effortValue) {
|
||||
return `${before}effort: ${effortValue}${eol}${after}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* #767 — Inject `disallowedTools: <value>` into the YAML frontmatter of a Claude .md agent.
|
||||
* Mirrors injectEffortFrontmatter: idempotent (skips if disallowedTools: already present),
|
||||
* inserts immediately before the closing `---`. Claude-only — never call for other runtimes,
|
||||
* which break on unknown frontmatter keys.
|
||||
*/
|
||||
function injectDisallowedToolsFrontmatter(content, disallowedValue) {
|
||||
// Detect the dominant EOL from the first line (the opening `---`).
|
||||
// If the very first `---` is followed by \r\n, treat the whole file as CRLF.
|
||||
const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
|
||||
|
||||
// Build a frontmatter-matching regex that tolerates an optional \r before
|
||||
// each \n, so we handle both LF and CRLF files without needing to normalise
|
||||
// the whole content.
|
||||
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
|
||||
const match = fmRe.exec(content);
|
||||
if (!match) return content; // no YAML frontmatter — leave unchanged
|
||||
|
||||
// Idempotency guard: don't insert a second disallowedTools: line.
|
||||
const fmBody = match[1]; // content between the two `---` lines
|
||||
if (/^disallowedTools:/m.test(fmBody)) return content;
|
||||
|
||||
// Locate the exact position of the closing `---` line so we can insert
|
||||
// before it using a simple string splice.
|
||||
const openLen = 3 + eol.length; // "---" + eol
|
||||
const closingStart = match.index + openLen + fmBody.length;
|
||||
|
||||
const before = content.slice(0, closingStart);
|
||||
const after = content.slice(closingStart);
|
||||
return `${before}disallowedTools: ${disallowedValue}${eol}${after}`;
|
||||
}
|
||||
|
||||
// #767 — Read-only verifier/auditor agents get a Claude-Code disallowedTools deny-list.
|
||||
// Group A (pure read-only) deny Write,Edit,MultiEdit. Group B report-writers Write one
|
||||
// output file so they deny only Edit,MultiEdit. gsd-nyquist-auditor is intentionally
|
||||
// excluded (it legitimately uses Write AND Edit to create/patch test files).
|
||||
const READONLY_AGENT_DISALLOWED_TOOLS = {
|
||||
'gsd-plan-checker': 'Write, Edit, MultiEdit',
|
||||
'gsd-integration-checker': 'Write, Edit, MultiEdit',
|
||||
'gsd-ui-checker': 'Write, Edit, MultiEdit',
|
||||
'gsd-verifier': 'Edit, MultiEdit',
|
||||
'gsd-doc-verifier': 'Edit, MultiEdit',
|
||||
'gsd-eval-auditor': 'Edit, MultiEdit',
|
||||
'gsd-ui-auditor': 'Edit, MultiEdit',
|
||||
};
|
||||
|
||||
/**
|
||||
* #2517 — Read a single GSD config file (defaults.json or per-project
|
||||
* config.json) into a plain object, returning null on missing/empty files
|
||||
@@ -10181,6 +10227,8 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
const _universalEffort = resolveInstallTimeEffort(_effortCfg, _agentName);
|
||||
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime('claude', _universalEffort).value;
|
||||
content = injectEffortFrontmatter(content, _renderedEffort);
|
||||
const _disallowedTools = READONLY_AGENT_DISALLOWED_TOOLS[_agentName];
|
||||
if (_disallowedTools) content = injectDisallowedToolsFrontmatter(content, _disallowedTools);
|
||||
}
|
||||
// #3677 — normalize retired `/gsd:<cmd>` colon refs in the agent body
|
||||
// to the canonical hyphen form `/gsd-<cmd>` for hyphen-`name:`
|
||||
|
||||
Reference in New Issue
Block a user