@sema-agent/core
Version:
Stateless, task-oriented AI agent core
501 lines (500 loc) • 28.3 kB
JavaScript
import { attest } from "./tool-policy.js";
import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
import { catalogRuleFaceOf, isShellFaceToolName, pathTargetOf } from "./tool-registry.js";
import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule, ruleSpellingOf } from "./permission-rule-syntax.js";
import { COMMAND_RULE_TOOL, NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, keyedValueAbsentReason, laneToolNamesOf, nameGrammarReads, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
import { pathFamilyOf } from "../tools/fs/safety.js";
import { argsAsTheToolReads, callTargetReadingOf, callTargetViewsOf } from "./effective-path-target.js";
import { canonicalize, UNCANONICALIZABLE } from "./canonical-json.js";
import { bindPathRules, judgeRuleRows, policyAnchors, rowsWhereTheyLead } from "./path-rule-binding.js";
import { ruleDenialSentence } from "./rule-denial-sentence.js";
import { heldValues } from "./tool-name-readings.js";
export { parsePermissionRule };
function describeBehavior(v) {
switch (typeof v) {
case "undefined": return "undefined";
case "string": {
const q = JSON.stringify(v);
return q.length > 40 ? q.slice(0, 37) + '..."' : q;
}
case "number":
case "boolean":
case "bigint": return String(v);
case "symbol": return "a symbol";
case "function": return "a function";
default: return v === null ? "null" : "an object";
}
}
export const DEFAULT_CAPS = {
maxRules: 256,
maxRuleLength: 512,
maxPatternLength: 256,
maxStars: 16,
maxScalarValueChars: 16_384,
};
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, wildcardMatch } from "./namespaced-rule-name.js";
export { isNamespacedCoveringRuleName, namespacedRuleNameCovers, wildcardMatch };
export function catalogRuleFaceView(toolName) {
const face = catalogRuleFaceOf(toolName);
if (face === undefined)
return undefined;
return { ...face, pathTargeting: pathTargetOf(toolName) !== undefined };
}
export const fileToolParamVocabulary = (canonicalTool) => {
const face = catalogRuleFaceOf(canonicalTool);
return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
};
function countStars(s) {
let n = 0;
for (const c of s)
if (c === "*")
n++;
return n;
}
function unreadableRuleIssue(text) {
const lenient = parsePermissionRule(text);
if (!lenient.toolName)
return { code: "invalid.empty_tool", message: "tool name cannot be empty" };
if (lenient.ruleContent === undefined && /[()\\]/.test(lenient.toolName)) {
return { code: "invalid.paren", message: "tool name still contains paren/escape characters — a mis-escaped rule would compile to a dead bare rule" };
}
const open = indexOfUnescaped(text, "(");
if (lenient.ruleContent === undefined && open !== -1) {
const close = lastIndexOfUnescaped(text, ")");
const legitimateBare = close === text.length - 1 && ["", "*"].includes(text.substring(open + 1, close));
if (!legitimateBare)
return { code: "invalid.paren", message: "malformed parentheses / trailing characters (would silently downgrade to a bare-tool rule)" };
}
return {
code: "invalid.tool_name",
message: `the tool name is a spelling no tool name can have — the rule syntax reads no rule in it (blanks around the rule, a character outside a tool name's spelling, or one before the "("), so the rule would name no tool; fix the entry`,
};
}
function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
const faceOf = (toolName) => {
if (ruleFaces !== undefined) {
const face = ruleFaces(toolName);
return { face, judged: true };
}
const face = catalogRuleFaceView(toolName);
return { face, judged: face !== undefined };
};
const byTool = new Map();
const command = { deny: [], ask: [] };
const namespacedCovering = { deny: [], ask: [] };
const issues = [];
const bad = (rule, code, message) => {
issues.push({ rule, code, message });
};
if (rules.length > caps.maxRules) {
return { byTool, command, namespacedCovering, issues: [{ rule: "", code: "invalid.cap_exceeded", message: `${rules.length} rules > maxRules ${caps.maxRules}` }] };
}
for (const r of rules) {
const text = r.rule;
const behavior = r.behavior;
if (behavior !== "deny" && behavior !== "ask") {
if (behavior === "allow")
bad(text, "unsupported.allow", "an allow rule does not belong to a policy — allow entries live on the allow layer: put the entry in `TaskSpec.allowedTools` (a request's) or `RunnerDeps.allowedTools` (the deployment's); deny / ask rules stay here");
else
bad(text, "invalid.behavior", `a rule's behavior is one of "deny" | "ask" — got ${describeBehavior(behavior)}; a misspelled verb would compile to a rule that silently never fires`);
continue;
}
const verb = behavior;
if (text.length > caps.maxRuleLength) {
bad(text, "invalid.cap_exceeded", `rule longer than ${caps.maxRuleLength} chars`);
continue;
}
const parsed = ruleSpellingOf(text);
if (parsed === undefined) {
const why = unreadableRuleIssue(text);
bad(text, why.code, why.message);
continue;
}
if (parsed.toolName === "*") {
bad(text, "invalid.tool_name", `\`*\` names no tool — a rule reaches the tool it names, so this one would compile and never match; name the tool, or state the answer for every call no rule reaches with \`defaultAction\``);
continue;
}
const toolName = parsed.toolName;
const parenthesised = parsed.ruleContent !== undefined || indexOfUnescaped(text, "(") !== -1;
if (toolName.startsWith(MCP_NAMESPACE.prefix) && parenthesised) {
bad(text, "unsupported.mcp_paren", "MCP rules do not support patterns in parentheses (CC parity); use the toolAxes system");
continue;
}
if (parenthesised && isNamespacedCoveringRuleName(toolName)) {
bad(text, "unsupported.covering_paren", `"${toolName}" reaches a SET of tools, and the generic param DSL is keyed by exact tool name — a parenthesised rule on it can never match. ` +
`Write the parenthesised rule against one exact tool name, or drop the parentheses to keep the covering rule.`);
continue;
}
const entry = byTool.get(toolName) ?? { bare: {}, param: { deny: [], ask: [] }, content: { deny: [], ask: [] } };
if (parsed.ruleContent === undefined) {
if (isNamespacedCoveringRuleName(toolName)) {
namespacedCovering[verb].push({ ruleName: toolName, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
continue;
}
entry.bare[verb] = { ruleText: text, source: r.source };
byTool.set(toolName, entry);
continue;
}
const content = parsed.ruleContent;
const colon = content.indexOf(":");
if (isShellFaceToolName(toolName)) {
const paramPart = colon > 0 ? content.slice(0, colon).trim() : "";
const declaresParam = (tool) => colon > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(paramPart) && (catalogRuleFaceOf(tool)?.params ?? []).includes(paramPart);
const looksLikeParamRule = declaresParam(toolName);
if (toolName !== COMMAND_RULE_TOOL && !looksLikeParamRule) {
const commandWordParam = declaresParam(COMMAND_RULE_TOOL) && !content.endsWith(":*");
bad(text, "invalid.command_rule", commandWordParam
? `"${text}" is a parameter rule on "${paramPart}", which ${toolName} does not declare (its rule parameters: ${(catalogRuleFaceOf(toolName)?.params ?? []).join(", ")}); a parameter rule reaches only the tool that declares the parameter, so ${COMMAND_RULE_TOOL}(${content}) is a rule on ${COMMAND_RULE_TOOL}'s own argument and does not reach ${toolName}. A shell command rule is written ${COMMAND_RULE_TOOL}(<command>) — it reaches ${toolName} and every other tool that runs a shell command by the command it runs.`
: `a shell command rule is written ${COMMAND_RULE_TOOL}(…) — it reaches ${toolName} and every other tool that runs a shell command by the command it runs; "${text}" names no rule upstream reads. Write ${COMMAND_RULE_TOOL}(${content}).`);
continue;
}
if (toolName === COMMAND_RULE_TOOL && (!looksLikeParamRule || content.endsWith(":*"))) {
const parsedCommand = parseRuleText(text, verb);
if ("reject" in parsedCommand) {
bad(text, "invalid.command_rule", `${parsedCommand.reject.code}: ${parsedCommand.reject.message}`);
continue;
}
command[verb].push({ parsed: parsedCommand.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
continue;
}
}
else {
const resolved = faceOf(toolName);
if (resolved.judged && resolved.face === undefined) {
bad(text, "unsupported.tool_unknown", `"${toolName}" is not a tool on this leg's roster — the rule would compile and never match`);
continue;
}
const face = resolved.face;
if (nameGrammarReads(toolName, content)) {
const parsedName = parseRuleText(text, verb);
if ("reject" in parsedName) {
bad(text, "invalid.command_rule", `${parsedName.reject.code}: ${parsedName.reject.message}`);
continue;
}
const param = NAME_RULE_FACES[toolName].param;
if (face?.params !== undefined && !face.params.includes(param)) {
bad(text, "unsupported.param_unknown", `"${param}" is not a parameter of ${toolName} on this leg (it carries ${[...face.params].sort().join(", ")}) — a ${toolName} rule is read against it, so the rule would compile and never match`);
continue;
}
entry.content[verb].push({ parsed: parsedName.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
byTool.set(toolName, entry);
continue;
}
if (face !== undefined && face.pathTargeting === true && colon <= 0) {
const parsedPath = parseRuleText(text, verb);
if ("reject" in parsedPath) {
bad(text, "invalid.command_rule", `${parsedPath.reject.code}: ${parsedPath.reject.message}`);
continue;
}
for (const base of ruleBasesNeeded(parsedPath.rule)) {
const declared = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
if (declared !== undefined && !isUsablePathBase(declared, pathFamilyOf(pathBases))) {
bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and the \`pathBases.${base === "cwd" && pathBases.cwd === undefined ? "root" : base}\` this policy declares is not an absolute path of its tree's family — the rule could not be judged on any call`);
}
}
if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
continue;
const faces = PATH_RULE_FACES[toolName]?.[verb];
entry.content[verb].push({ parsed: parsedPath.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}), ...(faces !== undefined ? { faces } : {}) });
byTool.set(toolName, entry);
continue;
}
if (face?.params !== undefined && colon > 0) {
const paramPart = content.slice(0, colon).trim();
if (!face.params.includes(paramPart)) {
if (face.nonScalar?.includes(paramPart)) {
bad(text, "unsupported.param_non_scalar", `"${paramPart}" is a parameter of ${toolName} but its value is an object or an array — the generic matcher compares scalars only; the rule would compile and never match`);
}
else {
bad(text, "unsupported.param_unknown", `"${paramPart}" is not a parameter of ${toolName} (it carries ${[...face.params].sort().join(", ")}) — the rule would compile and never match. ` +
`For a path glob use the tool-specific lane (\`${toolName}(src/**)\` is not the generic param DSL); a Windows path such as \`${toolName}(C:/foo)\` parses as the param "C".`);
}
continue;
}
}
}
if (colon <= 0) {
bad(text, "invalid.param_split", "param-level ruleContent must be `param:pattern`");
continue;
}
const param = content.slice(0, colon).trim();
const pattern = content.slice(colon + 1).trim();
if (param === "" || pattern === "") {
bad(text, "invalid.param_split", "param and pattern must both be non-empty");
continue;
}
if (pattern.length > caps.maxPatternLength || countStars(pattern) > caps.maxStars) {
bad(text, "invalid.cap_exceeded", `pattern exceeds maxPatternLength ${caps.maxPatternLength} or maxStars ${caps.maxStars}`);
continue;
}
if (faceOf(toolName).face?.primaryParams.includes(param) === true && primaryFieldGeneric === "reject") {
bad(text, "invalid.primary_field", `"${param}" is ${toolName}'s primary field — the generic matcher is a trim+FULL-STRING lexical match (no shell/path semantics); '${toolName}(${param}:git push*)' would NOT match 'cd /x && git push'. Opt in with primaryFieldGeneric:"allow-lexical" if you understand this.`);
continue;
}
entry.param[verb].push({ param, pattern, behavior: verb, ruleText: text, source: r.source });
byTool.set(toolName, entry);
}
return { byTool, command, namespacedCovering, issues };
}
export function validatePermissionRules(rules, opts) {
const caps = { ...DEFAULT_CAPS, ...opts?.caps };
return compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.ruleFaces, opts?.pathBases).issues;
}
function isPlainObjectArgs(args) {
return typeof args === "object" && args !== null && !Array.isArray(args);
}
function matchParamRules(rules, req, maxScalarValueChars) {
const args = req.args;
if (!isPlainObjectArgs(args))
return undefined;
for (const r of rules) {
if (!Object.hasOwn(args, r.param)) {
if (readingAbsorbs(req, r.param, r.pattern))
return r;
continue;
}
const raw = args[r.param];
if (raw === undefined || raw === null)
continue;
const t = typeof raw;
if (t === "object" || t === "function" || t === "symbol")
continue;
const value = String(raw).trim();
if (value.length > maxScalarValueChars)
return r;
if (wildcardMatch(r.pattern, value))
return r;
}
return undefined;
}
function readingAbsorbs(req, param, pattern) {
if (pattern.includes("*"))
return false;
try {
const read = canonicalize(argsAsTheToolReads(req, { ...req.args, [param]: pattern }));
return read !== UNCANONICALIZABLE && read === canonicalize(req.args);
}
catch {
return false;
}
}
function ruleDenial(req, ruleText, source) {
return ruleDenialSentence({ source: "permission", tool: req.toolName, rule: ruleText, ...(source ? { origin: source } : {}) });
}
function ruleAskMessage(ruleText, source) {
return `flagged by permission rule "${ruleText}"${source ? ` (${source})` : ""}`;
}
const NO_LEADS = new Map();
export function createPermissionRulePolicy(rules, opts) {
const caps = { ...DEFAULT_CAPS, ...opts?.caps };
const pathBases = opts?.pathBases ?? {};
const { byTool, command, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.ruleFaces, pathBases);
const setLevelCap = issues.some((i) => i.code === "invalid.cap_exceeded" && i.rule === "");
if (issues.length > 0 && ((opts?.onInvalidRule ?? "throw") === "throw" || setLevelCap)) {
const e = new Error(`createPermissionRulePolicy: ${issues.length} invalid/unsupported rule(s):\n` +
issues.map((i) => ` [${i.code}] ${i.rule || "(rule set)"}: ${i.message}`).join("\n"));
e.code = "config.invalid_permission_rules";
e.issues = issues;
throw e;
}
if (issues.length > 0) {
if (opts?.onIssue) {
for (const i of issues) {
try {
opts.onIssue(i);
}
catch {
}
}
}
else {
console.warn(`createPermissionRulePolicy: skipped ${issues.length} invalid/unsupported rule(s): ` +
issues.map((i) => `[${i.code}] ${i.rule}`).join("; "));
}
}
const defaultAction = opts?.defaultAction ?? "allow";
const nameSets = { deny: [], ask: [] };
for (const [toolName, entry] of byTool) {
if (entry.bare.deny !== undefined || entry.param.deny.length > 0 || entry.content.deny.length > 0)
nameSets.deny.push(toolName);
if (entry.bare.ask !== undefined || entry.param.ask.length > 0 || entry.content.ask.length > 0)
nameSets.ask.push(toolName);
}
for (const behavior of ["deny", "ask"])
if (command[behavior].length > 0 && !nameSets[behavior].includes(COMMAND_RULE_TOOL))
nameSets[behavior].push(COMMAND_RULE_TOOL);
for (const behavior of ["deny", "ask"]) {
for (const r of namespacedCovering[behavior])
nameSets[behavior].push(r.ruleName);
}
const coveringHit = (lane, toolName) => lane.length === 0 ? undefined : lane.find((r) => namespacedRuleNameCovers(r.ruleName, toolName));
const hasCovering = namespacedCovering.deny.length > 0 || namespacedCovering.ask.length > 0;
const faceRules = {
deny: [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined)),
ask: [...byTool.values()].flatMap((e) => e.content.ask.filter((r) => r.faces !== undefined)),
};
const contentLaneOf = (behavior, entry, shellCall, callFace) => [
...(entry?.content[behavior].filter((r) => r.faces === undefined) ?? []),
...(shellCall ? command[behavior] : []),
...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
];
const hasCommandRules = command.deny.length > 0 || command.ask.length > 0;
const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
const pathRuleOf = new Map();
for (const e of byTool.values()) {
for (const behavior of ["deny", "ask"]) {
for (const r of e.content[behavior]) {
if (r.parsed.match !== "path" && r.parsed.match !== "subpath")
continue;
pathRuleOf.set(r, { parsed: r.parsed, ruleText: r.ruleText, ...(behavior === "deny" && r.faces !== undefined ? { faces: r.faces } : {}) });
}
}
}
const pathSpecs = [...pathRuleOf.values()];
const bound = new Map();
const boundAt = (anchors) => {
const key = JSON.stringify([anchors.root ?? null, anchors.cwd ?? null, anchors.home ?? null]);
const prior = bound.get(key);
if (prior !== undefined)
return prior;
const rows = bindPathRules(pathSpecs, anchors);
const of = new Map();
[...pathRuleOf.keys()].forEach((r, i) => of.set(r, rows[i]));
const out = { rows, of };
bound.set(key, out);
return out;
};
const contentHit = (lane, req) => {
if (lane.length === 0)
return undefined;
const pathLane = lane.filter((r) => pathRuleOf.has(r));
if (pathLane.length > 0) {
const views = callTargetViewsOf(req, "tighten", { ...(pathBases.root !== undefined ? { root: pathBases.root } : {}), ...(pathBases.cwd !== undefined ? { cwd: pathBases.cwd } : {}) });
if (views !== undefined) {
const { of } = boundAt(policyAnchors(pathBases, req.pathRuleAnchors));
const rows = rowsWhereTheyLead(pathLane.map((r) => of.get(r)), callTargetReadingOf(req)?.leads ?? NO_LEADS);
const hit = judgeRuleRows(rows, views, "tighten");
const rule = hit === undefined ? undefined : pathLane.find((r) => of.get(r).spec === hit.row.spec);
if (hit !== undefined && hit.unreadable === undefined)
return { rule: rule, ...(hit.view !== undefined ? { view: hit.view } : {}) };
if (hit !== undefined)
return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req) ?? { rule: rule, unreadable: hit.unreadable };
}
}
return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req);
};
const judgeOthers = (lane, req) => {
const command = isPlainObjectArgs(req.args) ? req.args[commandParam] : undefined;
let unreadable;
for (const r of lane) {
if (r.parsed.match === "name" || r.parsed.match === "name_prefix") {
const named = nameRuleSubjectOf(r.parsed.tool, req.args);
if (named !== undefined && nameRuleAdmits(r.parsed, named))
return { rule: r };
continue;
}
if (typeof command !== "string")
continue;
const outcome = programRunReachOf(r.parsed, command);
if (outcome.reach === "reached")
return { rule: r };
if (outcome.reach === "unreadable")
unreadable ??= { rule: r, unreadable: outcome.reason };
}
return unreadable;
};
const keylessRefusal = (entry, req) => {
for (const r of [...entry.content.deny, ...entry.content.ask]) {
if (r.parsed.match !== "name" || nameRuleSubjectOf(r.parsed.tool, req.args) !== undefined)
continue;
const absent = keyedValueAbsentReason(r.parsed.tool);
if (absent !== undefined)
return ruleDenialSentence({ source: "permission", tool: req.toolName, rule: r.ruleText, ...(r.source ? { origin: r.source } : {}), detail: absent });
}
return undefined;
};
const unreadableAsk = (hit, lane, req) => ({
action: "ask",
message: `tool "${req.toolName}" needs approval: the call could not be read against the permission rule ${hit.rule.ruleText} (${hit.unreadable}) — a person must decide`,
ruleUnreadable: lane,
requiresRealApproval: true,
});
const entryFor = (req) => {
const entries = heldValues("policy.permissionRules", byTool, req);
if (entries.length <= 1)
return entries[0];
const bare = {};
for (const b of ["deny", "ask"]) {
const hit = entries.find((e) => e.bare[b] !== undefined)?.bare[b];
if (hit !== undefined)
bare[b] = hit;
}
return {
bare,
param: { deny: entries.flatMap((e) => e.param.deny), ask: entries.flatMap((e) => e.param.ask) },
content: { deny: entries.flatMap((e) => e.content.deny), ask: entries.flatMap((e) => e.content.ask) },
};
};
return {
nameSets: [nameSets],
...(pathSpecs.length > 0 ? { pathRules: (anchors) => boundAt(policyAnchors(pathBases, anchors)).rows } : {}),
check(req) {
const entry = entryFor(req);
const covering = hasCovering && protocolOf(req.toolName) !== undefined;
if (entry?.bare.deny) {
return { action: "deny", message: ruleDenial(req, entry.bare.deny.ruleText, entry.bare.deny.source) };
}
if (covering) {
const coveringDeny = coveringHit(namespacedCovering.deny, req.toolName);
if (coveringDeny)
return { action: "deny", message: ruleDenial(req, coveringDeny.ruleText, coveringDeny.source) };
}
const callFace = faceRules.deny.length > 0 || faceRules.ask.length > 0 ? callPathFaceOf(req) : undefined;
const shellCall = hasCommandRules && laneToolNamesOf(req).includes(COMMAND_RULE_TOOL);
const denyLane = contentLaneOf("deny", entry, shellCall, callFace);
const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
const denied = { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
const face = contentDeny.view !== undefined ? callPathFaceOf(req) : undefined;
return face !== undefined ? attest(denied, req, { pathDenial: { rule: contentDeny.rule.ruleText, view: contentDeny.view, face } }) : denied;
}
if (entry) {
const paramDeny = matchParamRules(entry.param.deny, req, caps.maxScalarValueChars);
if (paramDeny) {
return { action: "deny", message: ruleDenial(req, paramDeny.ruleText, paramDeny.source) };
}
}
const keyless = entry !== undefined ? keylessRefusal(entry, req) : undefined;
if (keyless !== undefined)
return { action: "deny", message: keyless };
if (contentDeny?.unreadable !== undefined)
return unreadableAsk({ rule: contentDeny.rule, unreadable: contentDeny.unreadable }, "content_deny", req);
if (entry) {
if (entry.bare.ask) {
return { action: "ask", message: ruleAskMessage(entry.bare.ask.ruleText, entry.bare.ask.source), matchedAskRule: entry.bare.ask.ruleText };
}
}
if (covering) {
const coveringAsk = coveringHit(namespacedCovering.ask, req.toolName);
if (coveringAsk)
return { action: "ask", message: ruleAskMessage(coveringAsk.ruleText, coveringAsk.source), matchedAskRule: coveringAsk.ruleText };
}
const askLane = contentLaneOf("ask", entry, shellCall, callFace);
const contentAsk = askLane.length > 0 ? contentHit(askLane, req) : undefined;
if (contentAsk !== undefined) {
if (contentAsk.unreadable !== undefined)
return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, "content_ask", req);
return { action: "ask", message: ruleAskMessage(contentAsk.rule.ruleText, contentAsk.rule.source), matchedAskRule: contentAsk.rule.ruleText };
}
if (entry) {
const paramAsk = matchParamRules(entry.param.ask, req, caps.maxScalarValueChars);
if (paramAsk) {
return { action: "ask", message: ruleAskMessage(paramAsk.ruleText, paramAsk.source), matchedAskRule: paramAsk.ruleText };
}
}
if (defaultAction === "allow")
return { action: "allow" };
return { action: defaultAction, message: `no permission rule matched "${req.toolName}" (default-${defaultAction} rule set)` };
},
};
}