mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-10 08:41:13 +00:00
Exec approvals: share host-effective policy snapshot
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
buildExecApprovalPendingReplyPayload,
|
||||
resolveExecApprovalAllowedDecisions,
|
||||
resolveExecApprovalRequestAllowedDecisions,
|
||||
resolveExecApprovalCommandDisplay,
|
||||
type ExecApprovalRequest,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
@@ -38,7 +38,7 @@ export function buildTelegramExecApprovalPendingPayload(params: {
|
||||
cwd: params.request.request.cwd ?? undefined,
|
||||
host: params.request.request.host === "node" ? "node" : "gateway",
|
||||
nodeId: params.request.request.nodeId ?? undefined,
|
||||
allowedDecisions: resolveExecApprovalAllowedDecisions({ ask: params.request.request.ask }),
|
||||
allowedDecisions: resolveExecApprovalRequestAllowedDecisions(params.request.request),
|
||||
expiresAtMs: params.request.expiresAtMs,
|
||||
nowMs: params.nowMs,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { buildPluginApprovalPendingReplyPayload } from "openclaw/plugin-sdk/appr
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
createChannelNativeApprovalRuntime,
|
||||
resolveExecApprovalAllowedDecisions,
|
||||
resolveExecApprovalRequestAllowedDecisions,
|
||||
type ExecApprovalChannelRuntime,
|
||||
} from "openclaw/plugin-sdk/infra-runtime";
|
||||
import { resolveExecApprovalCommandDisplay } from "openclaw/plugin-sdk/infra-runtime";
|
||||
@@ -125,9 +125,9 @@ export class TelegramExecApprovalHandler {
|
||||
cwd: (request as ExecApprovalRequest).request.cwd ?? undefined,
|
||||
host: (request as ExecApprovalRequest).request.host === "node" ? "node" : "gateway",
|
||||
nodeId: (request as ExecApprovalRequest).request.nodeId ?? undefined,
|
||||
allowedDecisions: resolveExecApprovalAllowedDecisions({
|
||||
ask: (request as ExecApprovalRequest).request.ask,
|
||||
}),
|
||||
allowedDecisions: resolveExecApprovalRequestAllowedDecisions(
|
||||
(request as ExecApprovalRequest).request,
|
||||
),
|
||||
expiresAtMs: request.expiresAtMs,
|
||||
nowMs,
|
||||
} satisfies ExecApprovalPendingReplyParams);
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
||||
import type { Command } from "commander";
|
||||
import JSON5 from "json5";
|
||||
import { readBestEffortConfig, type OpenClawConfig } from "../config/config.js";
|
||||
import { resolveExecPolicyScopeSummary } from "../infra/exec-approvals-effective.js";
|
||||
import { resolveExecPolicyScopeSnapshot } from "../infra/exec-approvals-effective.js";
|
||||
import {
|
||||
readExecApprovalsSnapshot,
|
||||
saveExecApprovals,
|
||||
@@ -31,7 +31,7 @@ type ConfigSnapshotLike = {
|
||||
};
|
||||
type ApprovalsTargetSource = "gateway" | "node" | "local";
|
||||
type EffectivePolicyReport = {
|
||||
scopes: ReturnType<typeof collectExecPolicySummaries>;
|
||||
scopes: ReturnType<typeof collectExecPolicySnapshots>;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
@@ -172,15 +172,16 @@ async function loadConfigForApprovalsTarget(params: {
|
||||
return snapshot.config && typeof snapshot.config === "object" ? snapshot.config : null;
|
||||
}
|
||||
|
||||
function collectExecPolicySummaries(params: { cfg: OpenClawConfig; approvals: ExecApprovalsFile }) {
|
||||
const summaries = [
|
||||
resolveExecPolicyScopeSummary({
|
||||
function collectExecPolicySnapshots(params: { cfg: OpenClawConfig; approvals: ExecApprovalsFile }) {
|
||||
const snapshots = [
|
||||
resolveExecPolicyScopeSnapshot({
|
||||
approvals: params.approvals,
|
||||
execConfig: params.cfg.tools?.exec,
|
||||
scopeExecConfig: params.cfg.tools?.exec,
|
||||
configPath: "tools.exec",
|
||||
scopeLabel: "tools.exec",
|
||||
}),
|
||||
];
|
||||
const globalExecConfig = params.cfg.tools?.exec;
|
||||
const configAgentIds = new Set((params.cfg.agents?.list ?? []).map((agent) => agent.id));
|
||||
const approvalAgentIds = Object.keys(params.approvals.agents ?? {}).filter(
|
||||
(agentId) => agentId !== "*" && agentId !== "default",
|
||||
@@ -188,17 +189,18 @@ function collectExecPolicySummaries(params: { cfg: OpenClawConfig; approvals: Ex
|
||||
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);
|
||||
summaries.push(
|
||||
resolveExecPolicyScopeSummary({
|
||||
snapshots.push(
|
||||
resolveExecPolicyScopeSnapshot({
|
||||
approvals: params.approvals,
|
||||
execConfig: agentConfig?.tools?.exec,
|
||||
scopeExecConfig: agentConfig?.tools?.exec,
|
||||
globalExecConfig,
|
||||
configPath: `agents.list.${agentId}.tools.exec`,
|
||||
scopeLabel: `agent:${agentId}`,
|
||||
agentId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return summaries;
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
function buildEffectivePolicyReport(params: {
|
||||
@@ -219,7 +221,7 @@ function buildEffectivePolicyReport(params: {
|
||||
};
|
||||
}
|
||||
return {
|
||||
scopes: collectExecPolicySummaries({
|
||||
scopes: collectExecPolicySnapshots({
|
||||
cfg: params.cfg,
|
||||
approvals: params.approvals,
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,8 @@ import { sanitizeExecApprovalDisplayText } from "../../infra/exec-approval-comma
|
||||
import type { ExecApprovalForwarder } from "../../infra/exec-approval-forwarder.js";
|
||||
import {
|
||||
DEFAULT_EXEC_APPROVAL_TIMEOUT_MS,
|
||||
isExecApprovalDecisionAllowed,
|
||||
resolveExecApprovalAllowedDecisions,
|
||||
resolveExecApprovalRequestAllowedDecisions,
|
||||
type ExecApprovalDecision,
|
||||
} from "../../infra/exec-approvals.js";
|
||||
import {
|
||||
@@ -150,6 +151,7 @@ export function createExecApprovalHandlers(
|
||||
host: host || null,
|
||||
security: p.security ?? null,
|
||||
ask: p.ask ?? null,
|
||||
allowedDecisions: resolveExecApprovalAllowedDecisions({ ask: p.ask ?? null }),
|
||||
agentId: effectiveAgentId ?? null,
|
||||
resolvedPath: p.resolvedPath ?? null,
|
||||
sessionKey: effectiveSessionKey ?? null,
|
||||
@@ -328,13 +330,8 @@ export function createExecApprovalHandlers(
|
||||
}
|
||||
const approvalId = resolvedId.id;
|
||||
const snapshot = manager.getSnapshot(approvalId);
|
||||
if (
|
||||
snapshot &&
|
||||
!isExecApprovalDecisionAllowed({
|
||||
decision,
|
||||
ask: snapshot.request.ask,
|
||||
})
|
||||
) {
|
||||
const allowedDecisions = resolveExecApprovalRequestAllowedDecisions(snapshot?.request);
|
||||
if (snapshot && !allowedDecisions.includes(decision)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
|
||||
@@ -23,7 +23,7 @@ import { resolveExecApprovalCommandDisplay } from "./exec-approval-command-displ
|
||||
import { formatExecApprovalExpiresIn } from "./exec-approval-reply.js";
|
||||
import { resolveExecApprovalSessionTarget } from "./exec-approval-session-target.js";
|
||||
import {
|
||||
resolveExecApprovalAllowedDecisions,
|
||||
resolveExecApprovalRequestAllowedDecisions,
|
||||
type ExecApprovalRequest,
|
||||
type ExecApprovalResolved,
|
||||
} from "./exec-approvals.js";
|
||||
@@ -200,7 +200,7 @@ function formatApprovalCommand(command: string): { inline: boolean; text: string
|
||||
}
|
||||
|
||||
function buildRequestMessage(request: ExecApprovalRequest, nowMs: number) {
|
||||
const allowedDecisions = resolveExecApprovalAllowedDecisions({ ask: request.request.ask });
|
||||
const allowedDecisions = resolveExecApprovalRequestAllowedDecisions(request.request);
|
||||
const decisionText = allowedDecisions.join("|");
|
||||
const lines: string[] = ["🔒 Exec approval required", `ID: ${request.id}`];
|
||||
const command = formatApprovalCommand(
|
||||
@@ -349,7 +349,7 @@ function buildExecPendingPayload(params: {
|
||||
approvalId: params.request.id,
|
||||
approvalSlug: params.request.id.slice(0, 8),
|
||||
text: buildRequestMessage(params.request, params.nowMs),
|
||||
allowedDecisions: resolveExecApprovalAllowedDecisions({ ask: params.request.request.ask }),
|
||||
allowedDecisions: resolveExecApprovalRequestAllowedDecisions(params.request.request),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
|
||||
import {
|
||||
DEFAULT_EXEC_APPROVAL_ASK_FALLBACK,
|
||||
resolveExecApprovalAllowedDecisions,
|
||||
type ExecApprovalDecision,
|
||||
maxAsk,
|
||||
minSecurity,
|
||||
resolveExecApprovalsFromFile,
|
||||
@@ -15,6 +18,10 @@ 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;
|
||||
@@ -25,7 +32,7 @@ export type ExecPolicyFieldSummary<TValue extends ExecSecurity | ExecAsk> = {
|
||||
note: string;
|
||||
};
|
||||
|
||||
export type ExecPolicyScopeSummary = {
|
||||
export type ExecPolicyScopeSnapshot = {
|
||||
scopeLabel: string;
|
||||
configPath: string;
|
||||
agentId?: string;
|
||||
@@ -35,16 +42,21 @@ export type ExecPolicyScopeSummary = {
|
||||
effective: ExecSecurity;
|
||||
source: string;
|
||||
};
|
||||
allowedDecisions: readonly ExecApprovalDecision[];
|
||||
};
|
||||
|
||||
export type ExecPolicyScopeSummary = Omit<ExecPolicyScopeSnapshot, "allowedDecisions">;
|
||||
|
||||
type ExecPolicyRequestedField = "security" | "ask";
|
||||
|
||||
function formatRequestedSource(params: {
|
||||
path: string;
|
||||
sourcePath: string;
|
||||
field: "security" | "ask";
|
||||
explicit: boolean;
|
||||
defaultValue: ExecSecurity | ExecAsk;
|
||||
}): string {
|
||||
return params.explicit
|
||||
? `${params.path}.${params.field}`
|
||||
: `OpenClaw default (${REQUESTED_DEFAULT_LABEL[params.field]})`;
|
||||
return params.sourcePath === "__default__"
|
||||
? `OpenClaw default (${params.defaultValue})`
|
||||
: `${params.sourcePath}.${params.field}`;
|
||||
}
|
||||
|
||||
type ExecPolicyField = "security" | "ask" | "askFallback";
|
||||
@@ -67,6 +79,32 @@ function readExecPolicyField(params: {
|
||||
}
|
||||
}
|
||||
|
||||
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: params.field && "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 resolveHostFieldSource(params: {
|
||||
hostPath: string;
|
||||
agentId?: string;
|
||||
@@ -90,6 +128,9 @@ function resolveHostFieldSource(params: {
|
||||
) {
|
||||
return `${params.hostPath} defaults.${params.field}`;
|
||||
}
|
||||
if (params.field === "askFallback") {
|
||||
return `OpenClaw default (${DEFAULT_EXEC_APPROVAL_ASK_FALLBACK})`;
|
||||
}
|
||||
return "inherits requested tool policy";
|
||||
}
|
||||
|
||||
@@ -118,36 +159,62 @@ function formatHostSource(params: {
|
||||
|
||||
export function resolveExecPolicyScopeSummary(params: {
|
||||
approvals: ExecApprovalsFile;
|
||||
execConfig?: { security?: ExecSecurity; ask?: ExecAsk } | undefined;
|
||||
scopeExecConfig?: ExecPolicyConfig | undefined;
|
||||
globalExecConfig?: ExecPolicyConfig | undefined;
|
||||
configPath: string;
|
||||
scopeLabel: string;
|
||||
agentId?: string;
|
||||
hostPath?: string;
|
||||
}): ExecPolicyScopeSummary {
|
||||
const requestedSecurity = params.execConfig?.security ?? DEFAULT_REQUESTED_SECURITY;
|
||||
const requestedAsk = params.execConfig?.ask ?? DEFAULT_REQUESTED_ASK;
|
||||
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,
|
||||
ask: requestedAsk,
|
||||
security: requestedSecurity.value,
|
||||
ask: requestedAsk.value,
|
||||
},
|
||||
});
|
||||
const hostPath = params.hostPath ?? DEFAULT_HOST_PATH;
|
||||
const effectiveSecurity = minSecurity(requestedSecurity, resolved.agent.security);
|
||||
const effectiveSecurity = minSecurity(requestedSecurity.value, resolved.agent.security);
|
||||
const effectiveAsk =
|
||||
resolved.agent.ask === "off" ? "off" : maxAsk(requestedAsk, resolved.agent.ask);
|
||||
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,
|
||||
requested: requestedSecurity.value,
|
||||
requestedSource: formatRequestedSource({
|
||||
path: params.configPath,
|
||||
sourcePath:
|
||||
requestedSecurity.sourcePath === "scope"
|
||||
? params.configPath
|
||||
: requestedSecurity.sourcePath,
|
||||
field: "security",
|
||||
explicit: params.execConfig?.security !== undefined,
|
||||
defaultValue: DEFAULT_REQUESTED_SECURITY,
|
||||
}),
|
||||
host: resolved.agent.security,
|
||||
hostSource: formatHostSource({
|
||||
@@ -158,16 +225,17 @@ export function resolveExecPolicyScopeSummary(params: {
|
||||
}),
|
||||
effective: effectiveSecurity,
|
||||
note:
|
||||
effectiveSecurity === requestedSecurity
|
||||
effectiveSecurity === requestedSecurity.value
|
||||
? "requested security applies"
|
||||
: "stricter host security wins",
|
||||
},
|
||||
ask: {
|
||||
requested: requestedAsk,
|
||||
requested: requestedAsk.value,
|
||||
requestedSource: formatRequestedSource({
|
||||
path: params.configPath,
|
||||
sourcePath:
|
||||
requestedAsk.sourcePath === "scope" ? params.configPath : requestedAsk.sourcePath,
|
||||
field: "ask",
|
||||
explicit: params.execConfig?.ask !== undefined,
|
||||
defaultValue: DEFAULT_REQUESTED_ASK,
|
||||
}),
|
||||
host: resolved.agent.ask,
|
||||
hostSource: formatHostSource({
|
||||
@@ -178,7 +246,7 @@ export function resolveExecPolicyScopeSummary(params: {
|
||||
}),
|
||||
effective: effectiveAsk,
|
||||
note: resolveAskNote({
|
||||
requestedAsk,
|
||||
requestedAsk: requestedAsk.value,
|
||||
hostAsk: resolved.agent.ask,
|
||||
effectiveAsk,
|
||||
}),
|
||||
@@ -192,5 +260,6 @@ export function resolveExecPolicyScopeSummary(params: {
|
||||
approvals: params.approvals,
|
||||
}),
|
||||
},
|
||||
allowedDecisions: resolveExecApprovalAllowedDecisions({ ask: effectiveAsk }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ export type ExecApprovalRequestPayload = {
|
||||
host?: string | null;
|
||||
security?: string | null;
|
||||
ask?: string | null;
|
||||
allowedDecisions?: readonly ExecApprovalDecision[];
|
||||
agentId?: string | null;
|
||||
resolvedPath?: string | null;
|
||||
sessionKey?: string | null;
|
||||
@@ -159,7 +160,7 @@ export const DEFAULT_EXEC_APPROVAL_TIMEOUT_MS = 1_800_000;
|
||||
|
||||
const DEFAULT_SECURITY: ExecSecurity = "deny";
|
||||
const DEFAULT_ASK: ExecAsk = "on-miss";
|
||||
const DEFAULT_ASK_FALLBACK: ExecSecurity = "deny";
|
||||
export const DEFAULT_EXEC_APPROVAL_ASK_FALLBACK: ExecSecurity = "deny";
|
||||
const DEFAULT_AUTO_ALLOW_SKILLS = false;
|
||||
const DEFAULT_SOCKET = "~/.openclaw/exec-approvals.sock";
|
||||
const DEFAULT_FILE = "~/.openclaw/exec-approvals.json";
|
||||
@@ -469,7 +470,7 @@ export function resolveExecApprovalsFromFile(params: {
|
||||
const wildcard = file.agents?.["*"] ?? {};
|
||||
const fallbackSecurity = params.overrides?.security ?? DEFAULT_SECURITY;
|
||||
const fallbackAsk = params.overrides?.ask ?? DEFAULT_ASK;
|
||||
const fallbackAskFallback = params.overrides?.askFallback ?? DEFAULT_ASK_FALLBACK;
|
||||
const fallbackAskFallback = params.overrides?.askFallback ?? DEFAULT_EXEC_APPROVAL_ASK_FALLBACK;
|
||||
const fallbackAutoAllowSkills = params.overrides?.autoAllowSkills ?? DEFAULT_AUTO_ALLOW_SKILLS;
|
||||
const resolvedDefaults: Required<ExecApprovalsDefaults> = {
|
||||
security: normalizeSecurity(defaults.security, fallbackSecurity),
|
||||
@@ -675,6 +676,19 @@ export function resolveExecApprovalAllowedDecisions(params?: {
|
||||
return DEFAULT_EXEC_APPROVAL_DECISIONS;
|
||||
}
|
||||
|
||||
export function resolveExecApprovalRequestAllowedDecisions(params?: {
|
||||
ask?: string | null;
|
||||
allowedDecisions?: readonly ExecApprovalDecision[] | readonly string[] | null;
|
||||
}): readonly ExecApprovalDecision[] {
|
||||
const explicit = Array.isArray(params?.allowedDecisions)
|
||||
? params.allowedDecisions.filter(
|
||||
(decision): decision is ExecApprovalDecision =>
|
||||
decision === "allow-once" || decision === "allow-always" || decision === "deny",
|
||||
)
|
||||
: [];
|
||||
return explicit.length > 0 ? explicit : resolveExecApprovalAllowedDecisions({ ask: params?.ask });
|
||||
}
|
||||
|
||||
export function isExecApprovalDecisionAllowed(params: {
|
||||
decision: ExecApprovalDecision;
|
||||
ask?: string | null;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export {
|
||||
DEFAULT_EXEC_APPROVAL_TIMEOUT_MS,
|
||||
resolveExecApprovalAllowedDecisions,
|
||||
resolveExecApprovalRequestAllowedDecisions,
|
||||
type ExecApprovalDecision,
|
||||
type ExecApprovalRequest,
|
||||
type ExecApprovalRequestPayload,
|
||||
|
||||
Reference in New Issue
Block a user