openclaw/src/infra/exec-approvals-effective.ts

263 lines
7.9 KiB
TypeScript

import type { OpenClawConfig } from "../config/config.js";
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
import {
DEFAULT_EXEC_APPROVAL_ASK_FALLBACK,
resolveExecApprovalAllowedDecisions,
type ExecApprovalDecision,
maxAsk,
minSecurity,
resolveExecApprovalsFromFile,
type ExecApprovalsFile,
type ExecAsk,
type ExecSecurity,
} from "./exec-approvals.js";
const DEFAULT_REQUESTED_SECURITY: ExecSecurity = "full";
const DEFAULT_REQUESTED_ASK: ExecAsk = "off";
const DEFAULT_HOST_PATH = "~/.openclaw/exec-approvals.json";
const REQUESTED_DEFAULT_LABEL = {
security: DEFAULT_REQUESTED_SECURITY,
ask: DEFAULT_REQUESTED_ASK,
} as const;
type ExecPolicyConfig = {
security?: ExecSecurity;
ask?: ExecAsk;
};
export type ExecPolicyFieldSummary<TValue extends ExecSecurity | ExecAsk> = {
requested: TValue;
requestedSource: string;
host: TValue;
hostSource: string;
effective: TValue;
note: string;
};
export type ExecPolicyScopeSnapshot = {
scopeLabel: string;
configPath: string;
agentId?: string;
security: ExecPolicyFieldSummary<ExecSecurity>;
ask: ExecPolicyFieldSummary<ExecAsk>;
askFallback: {
effective: ExecSecurity;
source: string;
};
allowedDecisions: readonly ExecApprovalDecision[];
};
export type ExecPolicyScopeSummary = Omit<ExecPolicyScopeSnapshot, "allowedDecisions">;
type ExecPolicyRequestedField = "security" | "ask";
function formatRequestedSource(params: {
sourcePath: string;
field: "security" | "ask";
defaultValue: ExecSecurity | ExecAsk;
}): string {
return params.sourcePath === "__default__"
? `OpenClaw default (${params.defaultValue})`
: `${params.sourcePath}.${params.field}`;
}
type ExecPolicyField = "security" | "ask" | "askFallback";
function resolveRequestedField<TValue extends ExecSecurity | ExecAsk>(params: {
field: ExecPolicyRequestedField;
scopeExecConfig?: ExecPolicyConfig;
globalExecConfig?: ExecPolicyConfig;
}): { value: TValue; sourcePath: string } {
const scopeValue = params.scopeExecConfig?.[params.field];
if (scopeValue !== undefined) {
return {
value: scopeValue as TValue,
sourcePath: "scope",
};
}
const globalValue = params.globalExecConfig?.[params.field];
if (globalValue !== undefined) {
return {
value: globalValue as TValue,
sourcePath: "tools.exec",
};
}
const defaultValue = REQUESTED_DEFAULT_LABEL[params.field] as TValue;
return {
value: defaultValue,
sourcePath: "__default__",
};
}
function formatHostFieldSource(params: {
hostPath: string;
field: ExecPolicyField;
sourceSuffix: string | null;
}): string {
if (params.sourceSuffix) {
return `${params.hostPath} ${params.sourceSuffix}`;
}
if (params.field === "askFallback") {
return `OpenClaw default (${DEFAULT_EXEC_APPROVAL_ASK_FALLBACK})`;
}
return "inherits requested tool policy";
}
function resolveAskNote(params: {
requestedAsk: ExecAsk;
hostAsk: ExecAsk;
effectiveAsk: ExecAsk;
}): string {
if (params.hostAsk === "off" && params.requestedAsk !== "off") {
return "host ask=off suppresses prompts";
}
if (params.effectiveAsk === params.requestedAsk) {
return "requested ask applies";
}
return "more aggressive ask wins";
}
export function collectExecPolicyScopeSnapshots(params: {
cfg: OpenClawConfig;
approvals: ExecApprovalsFile;
hostPath?: string;
}): ExecPolicyScopeSnapshot[] {
const snapshots = [
resolveExecPolicyScopeSnapshot({
approvals: params.approvals,
scopeExecConfig: params.cfg.tools?.exec,
configPath: "tools.exec",
hostPath: params.hostPath,
scopeLabel: "tools.exec",
}),
];
const globalExecConfig = params.cfg.tools?.exec;
const configAgentIds = new Set(
(params.cfg.agents?.list ?? [])
.filter((agent) => agent.id !== DEFAULT_AGENT_ID || agent.tools?.exec !== undefined)
.map((agent) => agent.id),
);
const approvalAgentIds = Object.keys(params.approvals.agents ?? {}).filter(
(agentId) => agentId !== "*" && agentId !== "default" && agentId !== DEFAULT_AGENT_ID,
);
const agentIds = Array.from(new Set([...configAgentIds, ...approvalAgentIds])).toSorted();
for (const agentId of agentIds) {
const agentConfig = params.cfg.agents?.list?.find((agent) => agent.id === agentId);
snapshots.push(
resolveExecPolicyScopeSnapshot({
approvals: params.approvals,
scopeExecConfig: agentConfig?.tools?.exec,
globalExecConfig,
configPath: `agents.list.${agentId}.tools.exec`,
hostPath: params.hostPath,
scopeLabel: `agent:${agentId}`,
agentId,
}),
);
}
return snapshots;
}
export function resolveExecPolicyScopeSummary(params: {
approvals: ExecApprovalsFile;
scopeExecConfig?: ExecPolicyConfig | undefined;
globalExecConfig?: ExecPolicyConfig | undefined;
configPath: string;
scopeLabel: string;
agentId?: string;
hostPath?: string;
}): ExecPolicyScopeSummary {
const snapshot = resolveExecPolicyScopeSnapshot(params);
const { allowedDecisions: _allowedDecisions, ...summary } = snapshot;
return summary;
}
export function resolveExecPolicyScopeSnapshot(params: {
approvals: ExecApprovalsFile;
scopeExecConfig?: ExecPolicyConfig | undefined;
globalExecConfig?: ExecPolicyConfig | undefined;
configPath: string;
scopeLabel: string;
agentId?: string;
hostPath?: string;
}): ExecPolicyScopeSnapshot {
const requestedSecurity = resolveRequestedField<ExecSecurity>({
field: "security",
scopeExecConfig: params.scopeExecConfig,
globalExecConfig: params.globalExecConfig,
});
const requestedAsk = resolveRequestedField<ExecAsk>({
field: "ask",
scopeExecConfig: params.scopeExecConfig,
globalExecConfig: params.globalExecConfig,
});
const resolved = resolveExecApprovalsFromFile({
file: params.approvals,
agentId: params.agentId,
overrides: {
security: requestedSecurity.value,
ask: requestedAsk.value,
},
});
const hostPath = params.hostPath ?? DEFAULT_HOST_PATH;
const effectiveSecurity = minSecurity(requestedSecurity.value, resolved.agent.security);
const effectiveAsk =
resolved.agent.ask === "off" ? "off" : maxAsk(requestedAsk.value, resolved.agent.ask);
return {
scopeLabel: params.scopeLabel,
configPath: params.configPath,
...(params.agentId ? { agentId: params.agentId } : {}),
security: {
requested: requestedSecurity.value,
requestedSource: formatRequestedSource({
sourcePath:
requestedSecurity.sourcePath === "scope"
? params.configPath
: requestedSecurity.sourcePath,
field: "security",
defaultValue: DEFAULT_REQUESTED_SECURITY,
}),
host: resolved.agent.security,
hostSource: formatHostFieldSource({
hostPath,
field: "security",
sourceSuffix: resolved.agentSources.security,
}),
effective: effectiveSecurity,
note:
effectiveSecurity === requestedSecurity.value
? "requested security applies"
: "stricter host security wins",
},
ask: {
requested: requestedAsk.value,
requestedSource: formatRequestedSource({
sourcePath:
requestedAsk.sourcePath === "scope" ? params.configPath : requestedAsk.sourcePath,
field: "ask",
defaultValue: DEFAULT_REQUESTED_ASK,
}),
host: resolved.agent.ask,
hostSource: formatHostFieldSource({
hostPath,
field: "ask",
sourceSuffix: resolved.agentSources.ask,
}),
effective: effectiveAsk,
note: resolveAskNote({
requestedAsk: requestedAsk.value,
hostAsk: resolved.agent.ask,
effectiveAsk,
}),
},
askFallback: {
effective: resolved.agent.askFallback,
source: formatHostFieldSource({
hostPath,
field: "askFallback",
sourceSuffix: resolved.agentSources.askFallback,
}),
},
allowedDecisions: resolveExecApprovalAllowedDecisions({ ask: effectiveAsk }),
};
}