File size: 4,092 Bytes
391c43e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | /**
* Per-agent write scoping. A restricted agent (e.g. the interview agent) may
* read anywhere but write only within a single directory. The guard lives next
* to the existing read-only gate in tool-registry; these are its pure,
* unit-testable helpers. Paths containing `..` are rejected outright β the VFS
* has no symlinks, so a plain reject is sufficient.
*/
const WRITE_COMMANDS = new Set(['ss', 'touch', 'mkdir', 'rm', 'rmdir', 'mv', 'cp']);
function hasRedirect(cmd: string[]): boolean {
return cmd.includes('>') || cmd.includes('>>');
}
/** Mirrors isWriteOperation() in tool-registry β which commands mutate the VFS. */
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;
}
/** Normalize a raw command-line path token to an absolute VFS path. */
function toAbsolute(p: string): string {
let t = p.replace(/^['"]|['"]$/g, '');
if (!t.startsWith('/')) t = '/' + t;
return t;
}
/**
* Extract the write target path(s) from a command, normalized to absolute.
* [] β the command does not write.
* null β the command writes but the target could not be determined (caller fails closed).
* [paths] β the write target(s).
*/
export function writeTargets(cmd: string[]): string[] | null {
if (!isWriteCommand(cmd)) return [];
// Redirect: target is the token after the last >/>>
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') {
// Writes the image to --out/-o when given, otherwise to /.generated/.
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') {
// sed -i [script] <file> β the file is the last non-flag arg
const file = nonFlags[nonFlags.length - 1];
return file ? [toAbsolute(file)] : null;
}
if (c === 'cp' || c === 'mv') {
// destination is the last non-flag arg; need at least source + dest
if (nonFlags.length < 2) return null;
return [toAbsolute(nonFlags[nonFlags.length - 1])];
}
// ss, touch, mkdir, rm, rmdir β non-flag args are the targets
if (nonFlags.length === 0) return null;
return nonFlags.map(toAbsolute);
}
/** True if `path` is a file strictly inside `scope` (and contains no `..`). */
export function isPathWithinScope(scope: string, path: string): boolean {
if (!scope) return true;
if (path.includes('..')) return false;
const s = scope.endsWith('/') ? scope : scope + '/';
// The scope directory itself is in-scope (e.g. mkdir/rmdir of the dir the
// agent writes into) β operating on it escapes nothing.
if (path === s.slice(0, -1)) return true;
return path.startsWith(s);
}
/** Decide whether a command's writes are permitted under an agent's writeScope. */
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 };
}
|