| |
| |
| |
| |
| |
| |
| |
|
|
| const WRITE_COMMANDS = new Set(['ss', 'touch', 'mkdir', 'rm', 'rmdir', 'mv', 'cp']); |
|
|
| function hasRedirect(cmd: string[]): boolean { |
| return cmd.includes('>') || cmd.includes('>>'); |
| } |
|
|
| |
| function isWriteCommand(cmd: string[]): boolean { |
| const c = cmd[0]; |
| if (WRITE_COMMANDS.has(c)) return true; |
| if (c === 'generate-image') return true; |
| if (c === 'sed' && cmd.includes('-i')) return true; |
| if (c === 'curl' && (cmd.includes('-o') || cmd.includes('--output'))) return true; |
| if (hasRedirect(cmd)) return true; |
| return false; |
| } |
|
|
| |
| function toAbsolute(p: string): string { |
| let t = p.replace(/^['"]|['"]$/g, ''); |
| if (!t.startsWith('/')) t = '/' + t; |
| return t; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function writeTargets(cmd: string[]): string[] | null { |
| if (!isWriteCommand(cmd)) return []; |
|
|
| |
| if (hasRedirect(cmd)) { |
| const idx = Math.max(cmd.lastIndexOf('>'), cmd.lastIndexOf('>>')); |
| const target = cmd[idx + 1]; |
| return target ? [toAbsolute(target)] : null; |
| } |
|
|
| const c = cmd[0]; |
| const rest = cmd.slice(1); |
| const nonFlags = rest.filter(a => !a.startsWith('-')); |
|
|
| if (c === 'curl') { |
| const i = cmd.findIndex(a => a === '-o' || a === '--output'); |
| const target = cmd[i + 1]; |
| return target ? [toAbsolute(target)] : null; |
| } |
|
|
| if (c === 'generate-image') { |
| |
| const i = cmd.findIndex(a => a === '--out' || a === '-o'); |
| if (i >= 0) { |
| const target = cmd[i + 1]; |
| return target ? [toAbsolute(target)] : null; |
| } |
| return ['/.generated/']; |
| } |
|
|
| if (c === 'sed') { |
| |
| const file = nonFlags[nonFlags.length - 1]; |
| return file ? [toAbsolute(file)] : null; |
| } |
|
|
| if (c === 'cp' || c === 'mv') { |
| |
| if (nonFlags.length < 2) return null; |
| return [toAbsolute(nonFlags[nonFlags.length - 1])]; |
| } |
|
|
| |
| if (nonFlags.length === 0) return null; |
| return nonFlags.map(toAbsolute); |
| } |
|
|
| |
| export function isPathWithinScope(scope: string, path: string): boolean { |
| if (!scope) return true; |
| if (path.includes('..')) return false; |
| const s = scope.endsWith('/') ? scope : scope + '/'; |
| |
| |
| if (path === s.slice(0, -1)) return true; |
| return path.startsWith(s); |
| } |
|
|
| |
| export function checkWriteScope( |
| cmd: string[], |
| scope: string | undefined, |
| ): { allowed: boolean; reason?: string } { |
| if (!scope) return { allowed: true }; |
| const targets = writeTargets(cmd); |
| if (targets === null) { |
| return { allowed: false, reason: `this agent may only write within ${scope}, and the write target could not be verified` }; |
| } |
| for (const t of targets) { |
| if (!isPathWithinScope(scope, t)) { |
| return { allowed: false, reason: `this agent may only write within ${scope} (attempted: ${t})` }; |
| } |
| } |
| return { allowed: true }; |
| } |
|
|