Skip to content

Commit 57fd218

Browse files
authored
Merge pull request #246 from peg/staging
2 parents e65b509 + bc37968 commit 57fd218

File tree

4 files changed

+301
-40
lines changed

4 files changed

+301
-40
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.9.11] - 2026-03-31
11+
12+
### Security
13+
14+
- **`openclaw.yaml`: closed bash/sh exec bypass**`bash *` and `sh *` were in the exec allowlist, allowing `bash -c 'rm -rf /'` to match an allow rule before hitting the destructive block. Both patterns removed.
15+
- **`openclaw.yaml`: closed curl/wget exec bypass**`curl *` and `wget *` wildcards allowed agents to exfiltrate via exec while `web_fetch` domain rules were correctly enforced. New `block-external-network-exec` rule denies all external curl/wget via exec; localhost requests remain allowed.
16+
- **`openclaw.yaml`: tightened git push**`git push origin *` no longer matches force-push (`--force`, `-f`) or branch deletion (`--delete`) variants; those surface for human approval.
17+
- **`openclaw.yaml`: tightened docker/kubectl** — replaced `docker *` and `kubectl *` wildcards with explicit safe subcommand lists; `docker run --privileged` and `kubectl delete` surface for approval instead of being allowed.
18+
19+
### Added
20+
21+
- **`openclaw.yaml`: `sessions_spawn` depth guard** — subagents cannot spawn further agents (prevents unbounded agent trees and lateral escalation). Main session spawning remains allowed.
22+
- **`openclaw.yaml`: `default_action: ask`** — novel tool calls (not matched by any policy rule) now surface for human review instead of silently failing. Fixes a major false-positive source for users with custom tools.
23+
- **`engine.go`: `default_action: ask` support**`parseDefaultAction` now accepts `ask` as a valid value.
24+
- **`policies/openclaw_test.go`** — 37 test cases covering all `openclaw.yaml` policy decisions.
25+
26+
### Changed
27+
28+
- **`openclaw.yaml`: credential reads → ask instead of deny**`.aws/credentials`, `.kube/config`, `.docker/config.json`, and `.env*` files now require human approval instead of hard-blocking. Absolute denies remain for SSH private keys, `.git-credentials`, `/etc/shadow`, `.gnupg`.
29+
- **`openclaw.yaml`: `.aws/config` allowed** — AWS config contains region/profile metadata, not secrets. No longer blocked.
30+
- **`openclaw.yaml`: exfil domains → ask instead of deny** — ngrok, webhook.site, requestbin, and similar services now prompt for approval (developers legitimately use these for local testing).
31+
1032
## [0.9.10] - 2026-03-30
1133

1234
### Added (OpenClaw Native Plugin — Primary Integration)

internal/engine/engine.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,8 @@ func (e *Engine) parseDefaultAction(s string) Action {
817817
return ActionDeny
818818
case "watch", "log": // "log" kept as deprecated alias
819819
return ActionWatch
820+
case "ask", "require_approval": // "require_approval" kept as deprecated alias
821+
return ActionAsk
820822
default:
821823
// If unspecified or invalid, default to deny (fail closed).
822824
return ActionDeny

policies/openclaw.yaml

Lines changed: 159 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# rampart setup openclaw --patch-tools (legacy dist patching)
1515

1616
version: "1"
17-
default_action: deny
17+
default_action: ask
1818

1919
policies:
2020
# ── Self-modification protection ──────────────────────────────────────
@@ -165,8 +165,30 @@ policies:
165165
- "go get *"
166166
- "go install *"
167167
- "go generate *"
168-
# Git operations
169-
- "git *"
168+
# Safe git subcommands only — not force-push, branch deletion, remote manipulation
169+
- "git status*"
170+
- "git log*"
171+
- "git diff*"
172+
- "git show*"
173+
- "git add*"
174+
- "git commit*"
175+
- "git checkout*"
176+
- "git switch*"
177+
- "git branch*"
178+
- "git fetch*"
179+
- "git pull*"
180+
- "git push origin *"
181+
- "git stash*"
182+
- "git tag*"
183+
- "git clone*"
184+
- "git init*"
185+
- "git remote -v"
186+
- "git remote get-url*"
187+
- "git worktree*"
188+
- "git merge*"
189+
- "git rebase*"
190+
- "git reset*"
191+
- "git revert*"
170192
# Node/npm/yarn
171193
- "node *"
172194
- "npm *"
@@ -207,26 +229,95 @@ policies:
207229
- "type *"
208230
- "env"
209231
- "printenv *"
210-
- "bash *"
211-
- "sh *"
212-
- "curl *"
213-
- "wget *"
214232
- "jq *"
215233
- "yq *"
216234
- "make *"
217-
- "docker *"
218-
- "kubectl *"
235+
# Safe docker subcommands — not run --privileged, not rm, not system prune
236+
- "docker build*"
237+
- "docker images*"
238+
- "docker ps*"
239+
- "docker logs*"
240+
- "docker inspect*"
241+
- "docker pull*"
242+
- "docker tag*"
243+
- "docker push*"
244+
- "docker compose*"
245+
- "docker-compose*"
246+
# Safe kubectl subcommands — not delete, not apply to prod namespaces
247+
- "kubectl get*"
248+
- "kubectl describe*"
249+
- "kubectl logs*"
250+
- "kubectl apply*"
251+
- "kubectl diff*"
252+
- "kubectl config*"
253+
- "kubectl version*"
254+
- "kubectl cluster-info*"
255+
# Local curl/wget only — network requests should use web_fetch
256+
- "curl http://localhost*"
257+
- "curl https://localhost*"
258+
- "curl http://127.0.0.1*"
259+
- "curl https://127.0.0.1*"
260+
- "curl http://0.0.0.0*"
261+
- "wget http://localhost*"
262+
- "wget https://localhost*"
263+
- "wget http://127.0.0.1*"
219264
command_not_matches:
220-
# Block destructive variants
221-
- "rm -rf /*"
222-
- "rm -rf ~/*"
223-
- "rm -rf /"
224-
- "rm -rf ~"
225-
- "chmod -R 777 /"
226-
- "dd **of=/dev/sd**"
227-
- "dd **of=/dev/nvme**"
265+
# Never allow force-push or branch deletion via git push
266+
- "git push *--force*"
267+
- "git push *-f *"
268+
- "git push *-f"
269+
- "git push *--delete*"
270+
- "git push *:*"
228271
message: "Safe exec command allowed"
229272

273+
# ── Block external network exec ─────────────────────────────────────────
274+
# curl/wget to external destinations should use web_fetch (policy-aware).
275+
# This catches exec-based exfiltration bypassing web_fetch domain rules.
276+
- name: block-external-network-exec
277+
description: "Block curl/wget to external hosts via exec — use web_fetch instead"
278+
match:
279+
tool: ["exec"]
280+
rules:
281+
- action: deny
282+
when:
283+
command_matches:
284+
- "curl *"
285+
- "wget *"
286+
command_not_matches:
287+
# Allow localhost/loopback
288+
- "curl http://localhost*"
289+
- "curl https://localhost*"
290+
- "curl http://127.0.0.1*"
291+
- "curl https://127.0.0.1*"
292+
- "curl http://0.0.0.0*"
293+
- "wget http://localhost*"
294+
- "wget https://localhost*"
295+
- "wget http://127.0.0.1*"
296+
# Allow rampart learn endpoint specifically
297+
- "curl*http://localhost:9090/v1/*"
298+
message: "External curl/wget blocked — use web_fetch tool for external requests (policy-aware)"
299+
300+
# ── Block force-push ───────────────────────────────────────────────────
301+
# Dedicated deny for force-push variants so compound commands
302+
# (e.g. "echo x && git push --force") can't bypass allow-rule exclusions.
303+
- name: block-force-push
304+
description: "Block git force-push and branch deletion regardless of command context"
305+
match:
306+
tool: ["exec"]
307+
rules:
308+
- action: deny
309+
when:
310+
command_matches:
311+
- "git push *--force*"
312+
- "git push *-f *"
313+
- "git push *-f"
314+
- "git push *--delete*"
315+
- "*git push *--force*"
316+
- "*git push *-f *"
317+
- "*git push *-f"
318+
- "*git push *--delete*"
319+
message: "Git force-push and branch deletion blocked — run manually if intentional"
320+
230321
# ── Block destructive exec ─────────────────────────────────────────────
231322
- name: block-destructive-exec
232323
description: "Block destructive filesystem and system commands"
@@ -252,27 +343,43 @@ policies:
252343
match:
253344
tool: ["read"]
254345
rules:
255-
- action: deny
346+
- action: ask
256347
when:
257348
path_matches:
258-
- "**/.ssh/id_*"
349+
# AWS credentials file contains secrets — require approval
259350
- "**/.aws/credentials"
260-
- "**/.aws/config"
351+
# kube/docker configs contain auth tokens — require approval
261352
- "**/.kube/config"
262353
- "**/.docker/config.json"
263-
- "**/.git-credentials"
264-
- "**/.netrc"
265-
- "**/.gnupg/**"
354+
# .env files may contain secrets — require approval
266355
- "**/.env"
267356
- "**/.env.*"
268-
- "**/etc/shadow"
269-
- "**/etc/passwd"
270357
path_not_matches:
271358
- "**/.ssh/*.pub"
272359
- "**/.env.example"
273360
- "**/.env.sample"
274361
- "**/.env.template"
362+
- "**/.env.local.example"
363+
message: "Credential file — human approval required before agent reads this"
364+
- action: deny
365+
when:
366+
path_matches:
367+
# Absolute denies — no legitimate agent use case
368+
- "**/.ssh/id_*"
369+
- "**/.git-credentials"
370+
- "**/.netrc"
371+
- "**/.gnupg/**"
372+
- "**/etc/shadow"
373+
- "**/etc/passwd"
374+
path_not_matches:
375+
- "**/.ssh/*.pub"
275376
message: "Credential file access blocked"
377+
- action: allow
378+
when:
379+
path_matches:
380+
# AWS config (not credentials) — region/profile metadata, not secrets
381+
- "**/.aws/config"
382+
message: "AWS config (non-secret metadata) allowed"
276383

277384
# ── web_fetch: allow known-safe domains ───────────────────────────────
278385
# Allow fetches to common dev/research domains; block exfil destinations.
@@ -312,20 +419,21 @@ policies:
312419
- "api.anthropic.com"
313420
- "api.openai.com"
314421
message: "Web fetch to known-safe domain allowed"
315-
- action: deny
422+
- action: ask
316423
when:
317424
domain_matches:
318-
# Known exfiltration services
425+
# Common dev/testing services — require approval (legitimate but risky)
319426
- "*.ngrok.io"
320427
- "ngrok.io"
321428
- "*.ngrok-free.app"
322-
- "*.requestbin.com"
323-
- "*.hookbin.com"
429+
- "ngrok-free.app"
324430
- "*.webhook.site"
325431
- "webhook.site"
432+
- "*.requestbin.com"
433+
- "*.hookbin.com"
326434
- "*.pipedream.net"
327435
- "*.beeceptor.com"
328-
message: "Potential exfiltration domain blocked"
436+
message: "Web request to tunneling/webhook service — requires human approval (potential exfiltration risk)"
329437

330438
# ── web_search: always allow ───────────────────────────────────────────
331439
- name: allow-web-search
@@ -397,6 +505,23 @@ policies:
397505
default: true
398506
message: "Canvas operation allowed"
399507

508+
# ── sessions_spawn: subagent spawning guard ───────────────────────────
509+
# Agents can spawn subagents but not into unrestricted agent IDs.
510+
# Subagents themselves cannot spawn further agents (depth limit).
511+
- name: sessions-spawn-guard
512+
description: "Allow agent spawning with depth guard — subagents cannot spawn"
513+
match:
514+
tool: ["sessions_spawn"]
515+
rules:
516+
- action: deny
517+
when:
518+
session_matches: ["subagent:*"]
519+
message: "Subagents cannot spawn further agents (depth limit enforced)"
520+
- action: allow
521+
when:
522+
default: true
523+
message: "Agent spawn allowed from main session"
524+
400525
# ── Subagent write guard ───────────────────────────────────────────────
401526
# Subagents cannot modify identity/config files.
402527
- name: openclaw-subagent-write-guard
@@ -434,13 +559,7 @@ policies:
434559
- "sk-[a-zA-Z0-9]{20,}"
435560
message: "Response blocked: contains credentials"
436561

437-
# ── Allow-unmatched catch-all (lowest priority) ────────────────────────
438-
# With default_action: deny, this is unreachable for non-matched rules.
439-
# Kept for documentation clarity. Do not remove.
440-
- name: allow-unmatched
441-
priority: 10000
442-
rules:
443-
- action: deny
444-
when:
445-
default: true
446-
message: "No matching OpenClaw policy rule — blocked by default"
562+
# NOTE: default_action: ask means any unmatched tool call surfaces for
563+
# human review rather than silently failing. This is intentional — it
564+
# reduces false positives for novel tools and operations not yet in the
565+
# allowlist while maintaining oversight.

0 commit comments

Comments
 (0)