openclaw/src/infra/system-run-approval-binding.ts
2026-03-02 01:12:53 +00:00

208 lines
5.9 KiB
TypeScript

import crypto from "node:crypto";
import type { SystemRunApprovalBinding, SystemRunApprovalPlan } from "./exec-approvals.js";
import { normalizeEnvVarKey } from "./host-env-security.js";
type NormalizedSystemRunEnvEntry = [key: string, value: string];
function normalizeString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
function normalizeStringArray(value: unknown): string[] {
return Array.isArray(value) ? value.map((entry) => String(entry)) : [];
}
export function normalizeSystemRunApprovalPlan(value: unknown): SystemRunApprovalPlan | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const candidate = value as Record<string, unknown>;
const argv = normalizeStringArray(candidate.argv);
if (argv.length === 0) {
return null;
}
return {
argv,
cwd: normalizeString(candidate.cwd),
rawCommand: normalizeString(candidate.rawCommand),
agentId: normalizeString(candidate.agentId),
sessionKey: normalizeString(candidate.sessionKey),
};
}
function normalizeSystemRunEnvEntries(env: unknown): NormalizedSystemRunEnvEntry[] {
if (!env || typeof env !== "object" || Array.isArray(env)) {
return [];
}
const entries: NormalizedSystemRunEnvEntry[] = [];
for (const [rawKey, rawValue] of Object.entries(env as Record<string, unknown>)) {
if (typeof rawValue !== "string") {
continue;
}
const key = normalizeEnvVarKey(rawKey, { portable: true });
if (!key) {
continue;
}
entries.push([key, rawValue]);
}
entries.sort((a, b) => a[0].localeCompare(b[0]));
return entries;
}
function hashSystemRunEnvEntries(entries: NormalizedSystemRunEnvEntry[]): string | null {
if (entries.length === 0) {
return null;
}
return crypto.createHash("sha256").update(JSON.stringify(entries)).digest("hex");
}
export function buildSystemRunApprovalEnvBinding(env: unknown): {
envHash: string | null;
envKeys: string[];
} {
const entries = normalizeSystemRunEnvEntries(env);
return {
envHash: hashSystemRunEnvEntries(entries),
envKeys: entries.map(([key]) => key),
};
}
export function buildSystemRunApprovalBinding(params: {
argv: unknown;
cwd?: unknown;
agentId?: unknown;
sessionKey?: unknown;
env?: unknown;
}): { binding: SystemRunApprovalBinding; envKeys: string[] } {
const envBinding = buildSystemRunApprovalEnvBinding(params.env);
return {
binding: {
argv: normalizeStringArray(params.argv),
cwd: normalizeString(params.cwd),
agentId: normalizeString(params.agentId),
sessionKey: normalizeString(params.sessionKey),
envHash: envBinding.envHash,
},
envKeys: envBinding.envKeys,
};
}
function argvMatches(expectedArgv: string[], actualArgv: string[]): boolean {
if (expectedArgv.length === 0 || expectedArgv.length !== actualArgv.length) {
return false;
}
for (let i = 0; i < expectedArgv.length; i += 1) {
if (expectedArgv[i] !== actualArgv[i]) {
return false;
}
}
return true;
}
export type SystemRunApprovalMatchResult =
| { ok: true }
| {
ok: false;
code: "APPROVAL_REQUEST_MISMATCH" | "APPROVAL_ENV_BINDING_MISSING" | "APPROVAL_ENV_MISMATCH";
message: string;
details?: Record<string, unknown>;
};
type SystemRunApprovalMismatch = Extract<SystemRunApprovalMatchResult, { ok: false }>;
const APPROVAL_REQUEST_MISMATCH_MESSAGE = "approval id does not match request";
function requestMismatch(details?: Record<string, unknown>): SystemRunApprovalMatchResult {
return {
ok: false,
code: "APPROVAL_REQUEST_MISMATCH",
message: APPROVAL_REQUEST_MISMATCH_MESSAGE,
details,
};
}
export function matchSystemRunApprovalEnvHash(params: {
expectedEnvHash: string | null;
actualEnvHash: string | null;
actualEnvKeys: string[];
}): SystemRunApprovalMatchResult {
if (!params.expectedEnvHash && !params.actualEnvHash) {
return { ok: true };
}
if (!params.expectedEnvHash && params.actualEnvHash) {
return {
ok: false,
code: "APPROVAL_ENV_BINDING_MISSING",
message: "approval id missing env binding for requested env overrides",
details: { envKeys: params.actualEnvKeys },
};
}
if (params.expectedEnvHash !== params.actualEnvHash) {
return {
ok: false,
code: "APPROVAL_ENV_MISMATCH",
message: "approval id env binding mismatch",
details: {
envKeys: params.actualEnvKeys,
expectedEnvHash: params.expectedEnvHash,
actualEnvHash: params.actualEnvHash,
},
};
}
return { ok: true };
}
export function matchSystemRunApprovalBinding(params: {
expected: SystemRunApprovalBinding;
actual: SystemRunApprovalBinding;
actualEnvKeys: string[];
}): SystemRunApprovalMatchResult {
if (!argvMatches(params.expected.argv, params.actual.argv)) {
return requestMismatch();
}
if (params.expected.cwd !== params.actual.cwd) {
return requestMismatch();
}
if (params.expected.agentId !== params.actual.agentId) {
return requestMismatch();
}
if (params.expected.sessionKey !== params.actual.sessionKey) {
return requestMismatch();
}
return matchSystemRunApprovalEnvHash({
expectedEnvHash: params.expected.envHash,
actualEnvHash: params.actual.envHash,
actualEnvKeys: params.actualEnvKeys,
});
}
export function missingSystemRunApprovalBinding(params: {
actualEnvKeys: string[];
}): SystemRunApprovalMatchResult {
return requestMismatch({
envKeys: params.actualEnvKeys,
});
}
export function toSystemRunApprovalMismatchError(params: {
runId: string;
match: SystemRunApprovalMismatch;
}): { ok: false; message: string; details: Record<string, unknown> } {
const details: Record<string, unknown> = {
code: params.match.code,
runId: params.runId,
};
if (params.match.details) {
Object.assign(details, params.match.details);
}
return {
ok: false,
message: params.match.message,
details,
};
}