Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 43 additions & 0 deletions src/agent/risk-classifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,49 @@ describe('classifyRisk — bash high', () => {
classifyRisk('bash', { command: 'curl https://install.sh |sh' }, ctx),
).toBe('high');
});

// ── outbound HTTP writes (#1280) ──────────────────────────────────────────
it('curl -X POST → high (outbound write method)', () => {
expect(
classifyRisk('bash', { command: 'curl -X POST https://api.example.com/users -H "Content-Type: application/json" -d \'{"name":"Alice"}\'' }, ctx),
).toBe('high');
});

it('curl -X PUT → high (outbound write method)', () => {
expect(
classifyRisk('bash', { command: 'curl -X PUT https://api.example.com/users/1 -d \'{"name":"Bob"}\'' }, ctx),
).toBe('high');
});

it('curl -X PATCH → high (outbound write method)', () => {
expect(
classifyRisk('bash', { command: 'curl -X PATCH https://api.example.com/users/1 -d \'{"active":false}\'' }, ctx),
).toBe('high');
});

it('curl -X DELETE → high (outbound write method)', () => {
expect(
classifyRisk('bash', { command: 'curl -X DELETE https://api.example.com/users/1' }, ctx),
).toBe('high');
});

it('curl --data → high (body payload implies outbound write)', () => {
expect(
classifyRisk('bash', { command: 'curl --data \'{"key":"val"}\' https://api.example.com/events' }, ctx),
).toBe('high');
});

it('curl -d → high (short form body payload)', () => {
expect(
classifyRisk('bash', { command: 'curl -d @payload.json https://api.example.com/hook' }, ctx),
).toBe('high');
});

it('wget --post-data → high (wget outbound POST)', () => {
expect(
classifyRisk('bash', { command: 'wget --post-data="msg=hello" https://api.example.com/notify' }, ctx),
).toBe('high');
});
});

// ---- bash medium-risk patterns -------------------------------------------
Expand Down
12 changes: 12 additions & 0 deletions src/agent/risk-classifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ const BASH_HIGH: readonly string[] = [
'| bash',
'|sh',
'|bash',
// Outbound HTTP write methods — autonomous agents posting to external APIs
// can send irreversible mutations (payments, emails, Webhooks) without a
// human in the loop. BASH_HIGH gates these behind the AFK approval prompt.
// Note: `curl https://… | bash` already matches `| bash` above; these entries
// catch curl-to-API patterns that don't pipe to a shell.
'curl -X POST',
'curl -X PUT',
'curl -X PATCH',
'curl -X DELETE',
'curl --data',
'curl -d ',
'wget --post-data',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle intervening options in the curl high-risk gate

In autonomous AFK sessions, these literal substrings only classify writes when the mutating flag immediately follows curl; common valid commands such as curl -H 'Content-Type: application/json' -X POST URL -d '{}' or curl URL -X DELETE remain medium, so afk-mode-gate.ts allows them without approval. The same gap affects curl -F and wget --post-file, which this commit explicitly recognizes as writes in the OBSERVE detector. Match curl/wget invocations and their write flags independently of option ordering rather than matching only these exact command prefixes.

Useful? React with 👍 / 👎.

];

/**
Expand Down
13 changes: 13 additions & 0 deletions src/agent/safe-destruct-detect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ describe('detectDestructiveCommands', () => {
['docker rm -f web', 'docker-destructive'],
['kubectl delete pod api-0', 'kubectl-delete'],
["psql -c 'DELETE FROM orders'", 'sql-delete-from'],
// outbound HTTP writes (#1280)
['curl -X POST https://api.example.com/users', 'curl-write-method'],
['curl -X PUT https://api.example.com/users/1', 'curl-write-method'],
['curl -X PATCH https://api.example.com/users/1 -d \'{}\'', 'curl-write-method'],
['curl -X DELETE https://api.example.com/users/1', 'curl-write-method'],
['curl -d \'{"k":"v"}\' https://api.example.com/hook', 'curl-data-flag'],
['curl --data @body.json https://api.example.com/events', 'curl-data-flag'],
['curl -F file=@photo.png https://api.example.com/upload', 'curl-data-flag'],
['wget --post-data="msg=hi" https://api.example.com/notify', 'curl-data-flag'],
])('OBSERVE: flags %j → %s', (command, expectedId) => {
expect(detectDestructiveCommands(command)).toContain(expectedId);
});
Expand Down Expand Up @@ -143,6 +152,10 @@ describe('createSafeDestructDetect (two-tier hook)', () => {
['docker system prune -af', 'docker-destructive'],
['kubectl delete pod api-0', 'kubectl-delete'],
['DELETE FROM sessions WHERE expired=1', 'sql-delete-from'],
// outbound HTTP writes (#1280) — OBSERVE, never block
['curl -X POST https://api.example.com/', 'curl-write-method'],
['curl -d \'{"k":"v"}\' https://api.example.com/events', 'curl-data-flag'],
['wget --post-data="x=1" https://api.example.com/hook', 'curl-data-flag'],
])('OBSERVE pattern %s returns approve, not block', (command, _id) => {
const decision: HookDecision = hook(preCtx(command));
expect(decision.decision).toBe('approve');
Expand Down
27 changes: 27 additions & 0 deletions src/agent/safe-destruct-patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,33 @@ export const DESTRUCTIVE_PATTERNS: readonly DestructivePattern[] = [
tier: 'observe',
},

// ── outbound HTTP writes (curl / wget) ─────────────────────────────────────
//
// Invariant: OBSERVE (not BLOCK) — curl writes are inner-loop for many agent
// workflows (posting to local dev servers, CI webhooks, self-hosted APIs).
// A hard block would generate unacceptable friction; OBSERVE records the event
// so audit logs capture outbound mutations without stopping the flow.
//
// Two patterns cover the common shapes:
// curl-write-method: explicit method override via -X (POST / PUT / PATCH / DELETE).
// curl-data-flag: body-payload flags (-d / --data / -F / --form) which imply
// a POST even when -X is omitted.
//
// wget --post-data is captured under curl-data-flag via a shared pattern that
// is checked after these two entries (see curl-data-flag regex).
// Regex uses [^|;&]* to stop at shell pipeline/compound boundaries so a piped
// command is not misattributed to the preceding curl invocation.
{
id: 'curl-write-method',
re: /\bcurl\b[^|;&]*\s-X\s+(POST|PUT|PATCH|DELETE)\b/i,
tier: 'observe',
Comment on lines +209 to +211

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize curl's long request option

Normal sessions do not record curl --request POST https://api.example.com because this regex accepts only the short -X spelling. This is not an exotic alternate syntax: curl --help all documents the option as -X, --request <method> Specify request method to use. Consequently an equivalent POST/PUT/PATCH/DELETE command produces no safe-destruct observation or audit event; include the long form, and preferably the other accepted argument spellings, in this pattern.

Useful? React with 👍 / 👎.

},
{
id: 'curl-data-flag',
re: /\bcurl\b[^|;&]*\s(-d\b|--data\b|-F\b|--form\b)|\bwget\b[^|;&]*\s(--post-data\b|--post-file\b)/i,
tier: 'observe',
},

// ── infra / containers ───────────────────────────────────────────────────────
//
// Invariant: docker-destructive and kubectl-delete stay OBSERVE because their
Expand Down
Loading