* Core command execution logic. Returns a structured result instead of HTTP Response. * Used by both the HTTP handler (handleCommand) and chain subcommand routing. * * Options: * skipRateCheck: true when called from chain (chain counts as 1 request) * skipActivity: true when called from cha
(
body: { command: string; args?: string[]; tabId?: number },
tokenInfo?: TokenInfo | null,
opts?: { skipRateCheck?: boolean; skipActivity?: boolean; chainDepth?: number },
)
| 940 | * chainDepth: recursion guard — reject nested chains (depth > 0 means inside a chain) |
| 941 | */ |
| 942 | async function handleCommandInternalImpl( |
| 943 | body: { command: string; args?: string[]; tabId?: number }, |
| 944 | tokenInfo?: TokenInfo | null, |
| 945 | opts?: { skipRateCheck?: boolean; skipActivity?: boolean; chainDepth?: number }, |
| 946 | ): Promise<CommandResult> { |
| 947 | const { args = [], tabId } = body; |
| 948 | const rawCommand = body.command; |
| 949 | |
| 950 | if (!rawCommand) { |
| 951 | return { status: 400, result: JSON.stringify({ error: 'Missing "command" field' }), json: true }; |
| 952 | } |
| 953 | |
| 954 | // ─── Alias canonicalization (before scope, watch, tab-ownership, dispatch) ─ |
| 955 | // Agent-friendly names like 'setcontent' route to canonical 'load-html'. Must |
| 956 | // happen BEFORE scope check so a read-scoped token calling 'setcontent' is still |
| 957 | // rejected (load-html lives in SCOPE_WRITE). Audit logging preserves rawCommand |
| 958 | // so the trail records what the agent actually typed. |
| 959 | const command = canonicalizeCommand(rawCommand); |
| 960 | const isAliased = command !== rawCommand; |
| 961 | |
| 962 | // ─── Recursion guard: reject nested chains ────────────────── |
| 963 | if (command === 'chain' && (opts?.chainDepth ?? 0) > 0) { |
| 964 | return { status: 400, result: JSON.stringify({ error: 'Nested chain commands are not allowed' }), json: true }; |
| 965 | } |
| 966 | |
| 967 | // ─── Scope check (for scoped tokens) ────────────────────────── |
| 968 | if (tokenInfo && tokenInfo.clientId !== 'root') { |
| 969 | if (!checkScope(tokenInfo, command)) { |
| 970 | return { |
| 971 | status: 403, json: true, |
| 972 | result: JSON.stringify({ |
| 973 | error: `Command "${command}" not allowed by your token scope`, |
| 974 | hint: `Your scopes: ${tokenInfo.scopes.join(', ')}. Ask the user to re-pair with --admin for eval/cookies/storage access.`, |
| 975 | }), |
| 976 | }; |
| 977 | } |
| 978 | |
| 979 | // `--out` writes the evaluate result to local disk, which is a WRITE |
| 980 | // capability distinct from the JS-exec (admin) capability js/eval need. |
| 981 | // Require write scope so an admin-but-not-write token can't write files. |
| 982 | if (hasOutArg(args) && !tokenInfo.scopes.includes('write')) { |
| 983 | return { |
| 984 | status: 403, json: true, |
| 985 | result: JSON.stringify({ |
| 986 | error: `"--out" writes to disk and requires the "write" scope`, |
| 987 | hint: `Your scopes: ${tokenInfo.scopes.join(', ')}. Re-pair with write access to use --out.`, |
| 988 | }), |
| 989 | }; |
| 990 | } |
| 991 | |
| 992 | // Domain check for navigation commands |
| 993 | if ((command === 'goto' || command === 'newtab') && args[0]) { |
| 994 | if (!checkDomain(tokenInfo, args[0])) { |
| 995 | return { |
| 996 | status: 403, json: true, |
| 997 | result: JSON.stringify({ |
| 998 | error: `Domain not allowed by your token scope`, |
| 999 | hint: `Allowed domains: ${tokenInfo.domains?.join(', ') || 'none configured'}`, |
no test coverage detected