@jovan158/vantage
Version:
See and control what your coding agent does
137 lines (136 loc) • 4.76 kB
JavaScript
// Action-type policy for problem ② — granular control by action type.
// Every tool call is classified into read / write / shell / network / other and
// judged against a configurable policy:
//
// allow no interference warn observe-only notice
// ask human must approve deny blocked outright
//
// "warn" is surfaced from the proxy's observation of the response stream.
// "ask"/"deny" are ENFORCED in src/hook.ts through the agent's PreToolUse hook —
// the proxy cannot do it, because a tool executes inside the agent and never
// travels through the proxy (CONCEPT.md §6b/c).
import fs from "node:fs";
import path from "node:path";
// Known tool names → action type: Claude Code's (which Copilot CLI reports
// too), then the ones the name patterns below would get wrong.
const TOOL_TYPES = {
Read: "read",
NotebookRead: "read",
Glob: "read",
Grep: "read",
LS: "read",
Write: "write",
Edit: "write",
MultiEdit: "write",
NotebookEdit: "write",
Bash: "shell",
BashOutput: "shell",
KillBash: "shell",
KillShell: "shell",
WebFetch: "network",
WebSearch: "network",
ls: "read", // pi
codesearch: "network", // OpenCode: searches the web for code
};
export function classifyTool(name) {
const known = TOOL_TYPES[name];
if (known)
return known;
const n = name.toLowerCase();
if (n.startsWith("mcp__"))
return "network"; // MCP tools reach external services
if (/(write|edit|create|delete|remove|move|rename|patch|apply)/.test(n))
return "write";
if (/(fetch|http|url|web|curl|request|download|upload)/.test(n))
return "network";
if (/(bash|shell|exec|command|terminal|process)/.test(n))
return "shell";
if (/(read|list|glob|grep|search|find|view|cat|show)/.test(n))
return "read";
return "other";
}
export const DEFAULT_POLICY = {
read: "allow",
write: "allow",
shell: "warn",
network: "warn",
other: "allow",
};
const TYPES = ["read", "write", "shell", "network", "other"];
const LEVELS = ["allow", "warn", "ask", "deny"];
function coerceLevel(v) {
return typeof v === "string" && LEVELS.includes(v) ? v : null;
}
/** True when any action type is set to an enforcing level. */
export function needsEnforcement(policy) {
return Object.values(policy).some((l) => l === "ask" || l === "deny");
}
// Load policy from .vantage/policy.json, then apply the VANTAGE_POLICY env
// override ("shell:warn,network:allow"). Unknown keys/levels are ignored.
export function loadPolicy(cwd, env = process.env) {
const policy = { ...DEFAULT_POLICY };
const file = path.join(cwd, ".vantage", "policy.json");
if (fs.existsSync(file)) {
try {
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
for (const t of TYPES) {
const lvl = coerceLevel(parsed[t]);
if (lvl)
policy[t] = lvl;
}
}
catch {
/* ignore malformed policy file */
}
}
const raw = env.VANTAGE_POLICY;
if (raw) {
for (const pair of raw.split(",")) {
const [k, v] = pair.split(":").map((s) => s.trim());
if (k && TYPES.includes(k)) {
const lvl = coerceLevel(v);
if (lvl)
policy[k] = lvl;
}
}
}
return policy;
}
// Emits a heads-up the first time each "warn" action type is observed in a
// session (no per-call spam).
export class PolicyWatcher {
seen = new Set();
policy;
constructor(policy) {
this.policy = policy;
}
observe(toolNames) {
const out = [];
for (const tool of toolNames) {
const type = classifyTool(tool);
const level = this.policy[type];
if (level === "allow" || this.seen.has(type))
continue;
this.seen.add(type);
out.push({
type,
tool,
level,
message: level === "warn"
? `${type} action used (${tool}) — policy 'warn' (observe-only, not blocked)`
: `${type} action used (${tool}) — policy '${level}' (enforced via agent hook)`,
});
}
return out;
}
}
export function summarizeActions(toolNames) {
const counts = { read: 0, write: 0, shell: 0, network: 0, other: 0 };
for (const name of toolNames)
counts[classifyTool(name)] += 1;
return counts;
}
export function formatActionSummary(counts) {
const parts = TYPES.filter((t) => counts[t] > 0).map((t) => `${t}×${counts[t]}`);
return parts.length ? parts.join(" · ") : null;
}