// Parses execution directives for approval, sandbox, and target settings. import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; import { type ExecAsk, type ExecSecurity, type ExecTarget, normalizeExecTarget, } from "../../../infra/exec-approvals-core.js"; import { removeDirectiveSpan, skipDirectiveArgPrefix, takeDirectiveToken, } from "../directive-parsing.js"; /** Parsed `/exec` directive state used to override execution policy for one turn. */ type ExecDirectiveParse = { cleaned: string; hasDirective: boolean; execHost?: ExecTarget; execSecurity?: ExecSecurity; execAsk?: ExecAsk; execNode?: string; rawExecHost?: string; rawExecSecurity?: string; rawExecAsk?: string; rawExecNode?: string; hasExecOptions: boolean; invalidHost: boolean; invalidSecurity: boolean; invalidAsk: boolean; invalidNode: boolean; }; function normalizeExecSecurity(value?: string): ExecSecurity | undefined { const normalized = normalizeOptionalLowercaseString(value); if (normalized === "deny" || normalized === "allowlist" || normalized === "full") { return normalized; } return undefined; } function normalizeExecAsk(value?: string): ExecAsk | undefined { const normalized = normalizeOptionalLowercaseString(value); if (normalized === "off" || normalized === "on-miss" || normalized === "always") { return normalized as ExecAsk; } return undefined; } function parseExecDirectiveArgs(raw: string): Omit< ExecDirectiveParse, "cleaned" | "hasDirective" > & { consumed: number; } { const len = raw.length; let i = skipDirectiveArgPrefix(raw); let consumed = i; let execHost: ExecTarget | undefined; let execSecurity: ExecSecurity | undefined; let execAsk: ExecAsk | undefined; let execNode: string | undefined; let rawExecHost: string | undefined; let rawExecSecurity: string | undefined; let rawExecAsk: string | undefined; let rawExecNode: string | undefined; let hasExecOptions = false; let invalidHost = false; let invalidSecurity = false; let invalidAsk = false; let invalidNode = false; const takeToken = (): string | null => { const res = takeDirectiveToken(raw, i); i = res.nextIndex; return res.token; }; const splitToken = (token: string): { key: string; value: string } | null => { const eq = token.indexOf("="); const colon = token.indexOf(":"); const idx = eq === -1 ? colon : colon === -1 ? eq : Math.min(eq, colon); if (idx === -1) { return null; } const key = normalizeOptionalLowercaseString(token.slice(0, idx)); const value = token.slice(idx + 1).trim(); if (!key) { return null; } return { key, value }; }; for (;;) { if (i >= len) { break; } const token = takeToken(); if (!token) { break; } const parsed = splitToken(token); if (!parsed) { break; } const { key, value } = parsed; if (key === "host") { rawExecHost = value; execHost = normalizeExecTarget(value) ?? undefined; if (!execHost) { invalidHost = true; } hasExecOptions = true; consumed = i; continue; } if (key === "security") { rawExecSecurity = value; execSecurity = normalizeExecSecurity(value); if (!execSecurity) { invalidSecurity = true; } hasExecOptions = true; consumed = i; continue; } if (key === "ask") { rawExecAsk = value; execAsk = normalizeExecAsk(value); if (!execAsk) { invalidAsk = true; } hasExecOptions = true; consumed = i; continue; } if (key === "node") { rawExecNode = value; const trimmed = value.trim(); if (!trimmed) { invalidNode = true; } else { execNode = trimmed; } hasExecOptions = true; consumed = i; continue; } break; } return { consumed, execHost, execSecurity, execAsk, execNode, rawExecHost, rawExecSecurity, rawExecAsk, rawExecNode, hasExecOptions, invalidHost, invalidSecurity, invalidAsk, invalidNode, }; } /** Extracts and removes `/exec` options from message text. */ export function extractExecDirective(body?: string): ExecDirectiveParse { if (!body) { return { cleaned: "", hasDirective: false, hasExecOptions: false, invalidHost: false, invalidSecurity: false, invalidAsk: false, invalidNode: false, }; } const re = /(?