| |
| 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"; |
|
|
| |
| 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, |
| }; |
| } |
|
|
| |
| export function extractExecDirective(body?: string): ExecDirectiveParse { |
| if (!body) { |
| return { |
| cleaned: "", |
| hasDirective: false, |
| hasExecOptions: false, |
| invalidHost: false, |
| invalidSecurity: false, |
| invalidAsk: false, |
| invalidNode: false, |
| }; |
| } |
| const re = /(?<!\S)\/exec(?=$|\s|:)/i; |
| const match = re.exec(body); |
| if (!match) { |
| return { |
| cleaned: body, |
| hasDirective: false, |
| hasExecOptions: false, |
| invalidHost: false, |
| invalidSecurity: false, |
| invalidAsk: false, |
| invalidNode: false, |
| }; |
| } |
| const start = match.index; |
| const argsStart = start + "/exec".length; |
| const parsed = parseExecDirectiveArgs(body.slice(argsStart)); |
| |
| const cleaned = removeDirectiveSpan(body, start, argsStart + parsed.consumed); |
| return { |
| cleaned, |
| hasDirective: true, |
| execHost: parsed.execHost, |
| execSecurity: parsed.execSecurity, |
| execAsk: parsed.execAsk, |
| execNode: parsed.execNode, |
| rawExecHost: parsed.rawExecHost, |
| rawExecSecurity: parsed.rawExecSecurity, |
| rawExecAsk: parsed.rawExecAsk, |
| rawExecNode: parsed.rawExecNode, |
| hasExecOptions: parsed.hasExecOptions, |
| invalidHost: parsed.invalidHost, |
| invalidSecurity: parsed.invalidSecurity, |
| invalidAsk: parsed.invalidAsk, |
| invalidNode: parsed.invalidNode, |
| }; |
| } |
|
|