Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ new version heading in the same commit.

## [Unreleased]

## [0.264.3] — 2026-07-24
### Fixed
- **Enricher: `rm -rf` inside the agent's OWN workdir (via an absolute path) is no longer hard-denied.**
A post-deploy fleet scan surfaced a residual false positive: an agent cleaning up a dir inside its own
home by absolute path — `rm -rf /home/<u>/…/agents/<a>/work/client-app/broken` — was still `destructive`,
because the v0.260 path-safety only whitelisted `/tmp` + relative paths, not the agent's own workdir
subtree. `isSafeDeletePath` now treats a strict SUBPATH of the workdir as safe (mirrors the
`outsideWorkdir` fact used for file writes). The workdir ROOT itself, a SIBLING agent's home, a `..`
escape, and any unrelated absolute/system path all stay denied — verified. 4 new golden cases (102/102).

## [0.264.2] — 2026-07-24
### Changed
- **Task room tabs are deep-linked in the URL.** The hash detail becomes `<taskId>/<tab>`
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agent-os",
"version": "0.264.2",
"version": "0.264.3",
"description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.",
"license": "MIT",
"type": "commonjs",
Expand Down
15 changes: 11 additions & 4 deletions src/governance/enricher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,19 @@ export function sanitizeForIntent(command: string): string {
* escape. UNSAFE (keeps it destructive) = any other absolute path, `~`/`$HOME`, a `..` escape, or an
* unresolved variable / command-substitution (`` marker) — we never green-light a delete we can't
* see the target of. */
function isSafeDeletePath(target: string): boolean {
function isSafeDeletePath(target: string, workdir?: string): boolean {
const t = target.trim();
if (!t || t.includes('') || t.includes('$(') || t.includes('`')) return false; // unknown target
if (t === '/' || t === '~' || t === '.' || t === '..' || t === '*') return false;
if (/(^|\/)\.\.(\/|$)/.test(t)) return false; // escapes upward
if (/^(\/tmp|\/private\/tmp|\/var\/folders)\//.test(t)) return true; // scratch roots
// A strict SUBPATH of the agent's own workdir is its sandbox — deleting there is its own work (like an
// in-folder file write; mirrors `outsideWorkdir`). Covers an absolute path into the agent's home, e.g.
// `rm -rf /home/<u>/.../agents/<a>/work/x`. The workdir ROOT itself stays gated (wiping the whole home).
if (workdir && t.startsWith('/')) {
const rel = path.relative(workdir, t);
if (rel && rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel)) return true;
}
if (t.startsWith('/') || t.startsWith('~')) return false; // any other absolute / home path
return true; // relative, no escape → inside the agent's cwd sandbox
}
Expand All @@ -172,7 +179,7 @@ function isSafeDeletePath(target: string): boolean {
* `VAR=value` assignments made INLINE in the same command (`SCRATCH=/tmp/x … rm -rf "$SCRATCH"`) so the
* common scratch-cleanup idiom is recognised. Returns false (→ stays destructive) on any unknown or
* unsafe target. Best-effort + conservative: it can only DOWNGRADE an `rm -rf` that is provably safe. */
function rmTargetsAllSafe(command: string): boolean {
function rmTargetsAllSafe(command: string, workdir?: string): boolean {
const vars: Record<string, string> = {};
for (const m of command.matchAll(/(?:^|[\s;&|(])([A-Za-z_][A-Za-z0-9_]*)=("[^"\n]*"|'[^'\n]*'|[^\s;&|)]+)/g)) {
vars[m[1]] = m[2].replace(/^['"]|['"]$/g, '');
Expand All @@ -188,7 +195,7 @@ function rmTargetsAllSafe(command: string): boolean {
targets.push(expand(tok));
}
}
return targets.length > 0 && targets.every(isSafeDeletePath);
return targets.length > 0 && targets.every((t) => isSafeDeletePath(t, workdir));
}

const SECRET_RE = /\b(gh[posru]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,})\b/g;
Expand Down Expand Up @@ -240,7 +247,7 @@ export function enrichArgs(
const otherDestructive = DESTRUCTIVE.some((re) => re.test(classifyText)) || (!!tool && DESTRUCTIVE_TOOL.test(tool));
// `rm -rf` is destructive only when a target is a real system/absolute path (or unresolvable) — a
// scratch/tmp/relative delete is routine agent work, not an irreversible world effect.
const dangerousRm = RM_RF.test(classifyText) && !rmTargetsAllSafe(classifyText);
const dangerousRm = RM_RF.test(classifyText) && !rmTargetsAllSafe(classifyText, workdir);
destructive = otherDestructive || dangerousRm;
}

Expand Down
48 changes: 48 additions & 0 deletions test/governance/conformance.json
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,54 @@
}
},
"expect": "never"
},
{
"name": "rm -rf a subpath of the agent workdir (absolute) is allowed",
"capability": "shell.exec",
"args": {
"tool": "Bash",
"input": {
"command": "rm -rf /home/ubuntu/agents/engineer/work/client-app/broken"
}
},
"workdir": "/home/ubuntu/agents/engineer",
"expect": "allow"
},
{
"name": "rm -rf the workdir ROOT is still denied",
"capability": "shell.exec",
"args": {
"tool": "Bash",
"input": {
"command": "rm -rf /home/ubuntu/agents/engineer"
}
},
"workdir": "/home/ubuntu/agents/engineer",
"expect": "never"
},
{
"name": "rm -rf a SIBLING agent home is still denied",
"capability": "shell.exec",
"args": {
"tool": "Bash",
"input": {
"command": "rm -rf /home/ubuntu/agents/qa/secrets"
}
},
"workdir": "/home/ubuntu/agents/engineer",
"expect": "never"
},
{
"name": "rm -rf .. escape from workdir still denied",
"capability": "shell.exec",
"args": {
"tool": "Bash",
"input": {
"command": "rm -rf /home/ubuntu/agents/engineer/../qa"
}
},
"workdir": "/home/ubuntu/agents/engineer",
"expect": "never"
}
],
"context": [
Expand Down
Loading