godcrm/backend/services/agent-tools/capability-gate.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

56 lines
2.5 KiB
JavaScript

/**
* ADR-165 WP-1 — Unified capability gate (pure decision layer).
*
* The per-agent tool allowlist used to be advisory on every path except the
* Claude Code CLI hook: it only shaped the tool list sent to the LLM, never
* verified the *invoked* tool. Under ADR-117 transitive delegation the LLM is
* the adversary on every path, so a prompt-injected agent could call any tool.
*
* This module is the decision half of the relocated enforcement point that now
* lives inside `executeTool()` (the universal choke point for all non-CLI
* paths). Kept pure — no DB, no imports — so it is unit-testable in isolation
* without pulling the executor's handler graph.
*
* Source of truth: callers thread the SAME resolved `allowedTools` list they
* built for the LLM into the execution `context`. So "what the model was
* offered" and "what the gate permits" can never drift.
*/
/**
* Resolve the set of permitted tool names from an execution context.
*
* @param {Object|null|undefined} context - executeTool() context.
* @returns {Set<string>|null} Set of allowed tool names, or `null` when no
* agent scope is present (system caller: MCP userId=1, human owner) — which
* the gate treats as unrestricted.
*/
export function resolveAllowlistNames(context) {
const list = context?.allowedTools;
if (!Array.isArray(list) || list.length === 0) return null;
const names = new Set();
for (const tool of list) {
// Accept OpenAI-shape ({ function: { name } }) and flat ({ name }) defs.
const name = tool?.function?.name || tool?.name;
if (name) names.add(name);
}
return names.size ? names : null;
}
/**
* Evaluate whether a tool call is permitted under the agent's capability scope.
*
* @param {string} toolName
* @param {Object|null|undefined} context - carries `allowedTools` when scoped.
* @param {boolean} enforce - true = hard block on violation; false = warn-only
* (violation flagged for telemetry but still permitted). First soak cycle
* runs warn-only, mirroring ADR-164's `deprecated_slug_resolve` WARN pattern.
* @returns {{ permitted: boolean, violation: boolean }}
*/
export function evaluateToolGate(toolName, context, enforce) {
const allowedNames = resolveAllowlistNames(context);
// No agent scope → system caller → unrestricted.
if (!allowedNames) return { permitted: true, violation: false };
if (allowedNames.has(toolName)) return { permitted: true, violation: false };
// Out of allowlist: always a violation for telemetry; blocked only in enforce.
return { permitted: !enforce, violation: true };
}