singularity-forge/src/resources/extensions/sf/execution-policy.js

187 lines
5.2 KiB
JavaScript

/**
* execution-policy.js — internal execution permission profiles for SF tool calls.
*
* Purpose: give autonomous run control and the machine surface a Codex/Crush-style
* vocabulary for filesystem, network, git, and mutation posture before those
* policies are enforced at every tool boundary.
*
* Consumer: write-gate, autonomous run-control hooks, machine-surface event DTOs,
* and future DB evidence rows that need stable permission-profile names instead
* of ad hoc booleans.
*/
import { shouldBlockQueueExecutionInSnapshot } from "./bootstrap/write-gate.js";
import { resolvePermissionProfile } from "./operating-model.js";
import { classifyCommand } from "./safety/destructive-guard.js";
export const EXECUTION_POLICY_PROFILES = {
restricted: {
id: "restricted",
permissionProfile: "restricted",
filesystem: "read-mostly",
network: "read-only",
git: "read-only",
mutation: "planning-artifacts-only",
},
normal: {
id: "normal",
permissionProfile: "normal",
filesystem: "workspace-write",
network: "allowed",
git: "normal",
mutation: "workspace",
},
trusted: {
id: "trusted",
permissionProfile: "trusted",
filesystem: "workspace-write",
network: "allowed",
git: "normal",
mutation: "workspace",
},
unrestricted: {
id: "unrestricted",
permissionProfile: "unrestricted",
filesystem: "danger-full-access",
network: "allowed",
git: "dangerous",
mutation: "host",
},
};
/**
* Resolve a named execution policy profile.
*
* Purpose: normalize unknown or missing policy names to the safest default
* profile so callers can fail closed.
*
* Consumer: tool-call classifiers and future typed headless events.
*/
export function resolveExecutionPolicyProfile(profileId) {
const id = resolvePermissionProfile(profileId);
return EXECUTION_POLICY_PROFILES[id] ?? EXECUTION_POLICY_PROFILES.restricted;
}
function classifyBashRisk(command) {
const destructive = classifyCommand(command);
if (destructive.destructive) {
return { risk: "destructive", labels: destructive.labels };
}
if (
/\b(git\s+push|npm\s+publish|docker\s+push|kubectl\s+apply|terraform\s+apply)\b/.test(
command,
)
) {
return { risk: "external-mutation", labels: [] };
}
if (/\b(curl|wget|fetch|gh\s+api)\b/.test(command)) {
return { risk: "network", labels: [] };
}
return { risk: "normal", labels: [] };
}
/**
* Classify a tool call under a named execution profile.
*
* Purpose: produce deterministic allow/deny/risk metadata before tool calls are
* executed, without coupling policy consumers to queue-mode implementation
* details.
*
* Consumer: auto/headless safety gates and execution evidence writers.
*/
export function classifyExecutionPolicyCall(profileId, toolName, input = "") {
const profile = resolveExecutionPolicyProfile(profileId);
if (profile.id === "restricted") {
const queueDecision = shouldBlockQueueExecutionInSnapshot(
{
verifiedDepthMilestones: [],
activeQueuePhase: true,
pendingGateId: null,
},
toolName,
input,
true,
);
if (queueDecision.block) {
return {
profile: profile.id,
permissionProfile: profile.permissionProfile,
allowed: false,
reason: queueDecision.reason,
risk: "mutation",
destructiveLabels: [],
};
}
}
const bashRisk =
toolName === "bash"
? classifyBashRisk(input)
: { risk: "normal", labels: [] };
const allowed =
profile.id === "unrestricted" ||
bashRisk.risk !== "destructive" ||
profile.mutation !== "planning-artifacts-only";
return {
profile: profile.id,
permissionProfile: profile.permissionProfile,
allowed,
reason: allowed ? "allowed" : "destructive command requires approval",
risk: bashRisk.risk,
destructiveLabels: bashRisk.labels,
};
}
/**
* Extract the policy-relevant input string from a tool-call event.
*
* Purpose: avoid leaking full structured tool payloads into policy logs while
* preserving the command or path needed for deterministic classification.
*
* Consumer: execution-policy journal events.
*/
export function extractExecutionPolicyInput(toolName, input) {
if (!input || typeof input !== "object") return "";
if (toolName === "bash") return String(input.command ?? "");
if (toolName === "write" || toolName === "edit") {
return String(input.path ?? "");
}
if (toolName === "sf_exec") return String(input.script ?? "");
return "";
}
/**
* Build a journal entry for a tool-call execution-policy decision.
*
* Purpose: make policy decisions inspectable before broad enforcement, matching
* the comparison-survey direction from Codex/Crush without changing runtime
* permissions in this slice.
*
* Consumer: SF autonomous mode tool_call hook.
*/
export function buildExecutionPolicyJournalEntry(args) {
const input = extractExecutionPolicyInput(
args.event.toolName,
args.event.input,
);
const decision = classifyExecutionPolicyCall(
args.profileId,
args.event.toolName,
input,
);
return {
ts: args.now?.() ?? new Date().toISOString(),
flowId:
args.flowId ??
`execution-policy:${args.event.toolCallId ?? args.event.toolName}`,
seq: 0,
eventType: "execution-policy-decision",
unitType: args.unit?.type,
unitId: args.unit?.id,
data: {
toolCallId: args.event.toolCallId,
toolName: args.event.toolName,
input,
decision,
},
};
}