damage-control-dev 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. damage_control/__init__.py +9 -0
  2. damage_control/__main__.py +7 -0
  3. damage_control/adapter.py +179 -0
  4. damage_control/audit.py +432 -0
  5. damage_control/breakglass.py +155 -0
  6. damage_control/claude_code.py +148 -0
  7. damage_control/cli.py +173 -0
  8. damage_control/engine.py +10656 -0
  9. damage_control/groups.py +114 -0
  10. damage_control/harness_config.py +205 -0
  11. damage_control/hook.py +54 -0
  12. damage_control/install.py +618 -0
  13. damage_control/packs.py +219 -0
  14. damage_control/policy.py +323 -0
  15. damage_control/profile.py +177 -0
  16. damage_control/rules/aws.json +85 -0
  17. damage_control/rules/aws.yaml +76 -0
  18. damage_control/rules/cloud-hosting.json +162 -0
  19. damage_control/rules/cloud-hosting.yaml +163 -0
  20. damage_control/rules/containers.json +67 -0
  21. damage_control/rules/containers.yaml +69 -0
  22. damage_control/rules/control-plane.json +7 -0
  23. damage_control/rules/control-plane.yaml +21 -0
  24. damage_control/rules/core.json +332 -0
  25. damage_control/rules/core.yaml +441 -0
  26. damage_control/rules/databases.json +117 -0
  27. damage_control/rules/databases.yaml +123 -0
  28. damage_control/rules/firebase.json +34 -0
  29. damage_control/rules/firebase.yaml +30 -0
  30. damage_control/rules/gcp.json +60 -0
  31. damage_control/rules/gcp.yaml +57 -0
  32. damage_control/rules/git.json +119 -0
  33. damage_control/rules/git.yaml +120 -0
  34. damage_control/rules/infrastructure.json +69 -0
  35. damage_control/rules/infrastructure.yaml +66 -0
  36. damage_control/rules/mcp-tools.json +11 -0
  37. damage_control/rules/mcp-tools.yaml +47 -0
  38. damage_control/rules/outbound.json +40 -0
  39. damage_control/rules/outbound.yaml +58 -0
  40. damage_control/rules/payloads.json +120 -0
  41. damage_control/rules/payloads.yaml +155 -0
  42. damage_control/rules/publish.json +46 -0
  43. damage_control/rules/publish.yaml +46 -0
  44. damage_control/rules/registry.json +10 -0
  45. damage_control/rules/registry.yaml +11 -0
  46. damage_control/rules/remote.json +31 -0
  47. damage_control/rules/remote.yaml +53 -0
  48. damage_control/rules/tmux.json +17 -0
  49. damage_control/rules/tmux.yaml +16 -0
  50. damage_control/ruleset.py +94 -0
  51. damage_control/tooldefs/aws.json +140 -0
  52. damage_control/tooldefs/aws.yaml +119 -0
  53. damage_control/tooldefs/docker.json +146 -0
  54. damage_control/tooldefs/docker.yaml +123 -0
  55. damage_control/tooldefs/gcp.json +186 -0
  56. damage_control/tooldefs/gcp.yaml +158 -0
  57. damage_control/tooldefs/gh.json +172 -0
  58. damage_control/tooldefs/gh.yaml +145 -0
  59. damage_control/tooldefs/git.json +166 -0
  60. damage_control/tooldefs/git.yaml +143 -0
  61. damage_control/tooldefs/kubectl.json +101 -0
  62. damage_control/tooldefs/kubectl.yaml +85 -0
  63. damage_control/tooldefs/npm.json +77 -0
  64. damage_control/tooldefs/npm.yaml +65 -0
  65. damage_control/tooldefs/terraform.json +68 -0
  66. damage_control/tooldefs/terraform.yaml +57 -0
  67. damage_control/tooldefs/uv.json +112 -0
  68. damage_control/tooldefs/uv.yaml +97 -0
  69. damage_control_dev-0.1.0.dist-info/METADATA +15 -0
  70. damage_control_dev-0.1.0.dist-info/RECORD +74 -0
  71. damage_control_dev-0.1.0.dist-info/WHEEL +4 -0
  72. damage_control_dev-0.1.0.dist-info/entry_points.txt +2 -0
  73. damage_control_dev-0.1.0.dist-info/licenses/LICENSE +201 -0
  74. damage_control_dev-0.1.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,58 @@
1
+ # Outbound communication damage control rules
2
+ #
3
+ # Outward-facing "send" verbs — email, SMS, push. These are irreversible (you
4
+ # can't un-send) and reach real people, so they must NOT sail through an
5
+ # UNATTENDED (scheduler) session silently. They're classified `ask: true`:
6
+ # - interactive bypass/auto session → allow (no friction, the common case)
7
+ # - interactive non-bypass session → confirm prompt
8
+ # - UNATTENDED (AGENTWIRE_UNATTENDED) → fail closed: block + email owner,
9
+ # unless the rule id is on the task's `unattended_allow`.
10
+ #
11
+ # These patterns gate the Bash shell-out (an agent or a scheduled task's `post:`
12
+ # step running `agentwire email`/`quo`). Agents inside agentwire sessions usually
13
+ # send via the MCP tools `email_send` / `quo_send` instead — those are gated by
14
+ # the dedicated `mcp-tool-damage-control.py` PreToolUse hook (#457), which
15
+ # synthesizes the equivalent `agentwire email`/`quo` command and reuses
16
+ # `outbound.agentwire-email` / `outbound.agentwire-quo` below verbatim (same IDs,
17
+ # same `unattended_allow`). See docs/wiki/internals/damage-control.md.
18
+
19
+ bashToolPatterns:
20
+ # ---------------------------------------------------------------------------
21
+ # TWILIO (SMS / messaging)
22
+ # ---------------------------------------------------------------------------
23
+ - pattern: '\btwilio\s+api[:\w.]*messages[:\w.]*create\b'
24
+ reason: twilio messages create (sends an SMS/MMS)
25
+ ask: true
26
+ id: outbound.twilio-messages
27
+ anchored: true
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # AWS messaging (SES email, SNS publish)
31
+ # ---------------------------------------------------------------------------
32
+ - pattern: '\baws\s+ses(v2)?\s+send-email\b'
33
+ reason: aws ses send-email (sends an external email)
34
+ ask: true
35
+ id: outbound.aws-ses
36
+ anchored: true
37
+
38
+ - pattern: '\baws\s+sns\s+publish\b'
39
+ reason: aws sns publish (sends an SMS/notification)
40
+ ask: true
41
+ id: outbound.aws-sns
42
+ anchored: true
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # GENERIC MAIL TRANSPORTS
46
+ # ---------------------------------------------------------------------------
47
+ - pattern: '\bsendmail\b'
48
+ reason: sendmail (sends external email)
49
+ ask: true
50
+ id: outbound.sendmail
51
+ anchored: true
52
+
53
+ - pattern: '\b(mail|mailx)\s+(?:(?!--(?:\s|$))[^\s;&|]+\s+)*?-s\b'
54
+ pattern_src: '\b(mail|mailx)\s+{ARGS}-s\b'
55
+ reason: mail -s (sends external email)
56
+ ask: true
57
+ id: outbound.mail
58
+ anchored: true
@@ -0,0 +1,120 @@
1
+ {
2
+ "bashToolPatterns": [
3
+ {
4
+ "pattern": "shutil\\.rmtree",
5
+ "reason": "shutil.rmtree recursive delete (equivalent to rm -rf)",
6
+ "id": "core.shutil-rmtree-recursive-delete-equivalent-to-rm-rf",
7
+ "anchored": false
8
+ },
9
+ {
10
+ "pattern": "\\bperl\\b.*\\bunlink\\b",
11
+ "reason": "perl unlink delete (use git clean or manual cleanup)",
12
+ "bypassable": true,
13
+ "id": "core.perl-unlink-delete-use-git-clean-or-manual-cleanup",
14
+ "anchored": false
15
+ },
16
+ {
17
+ "pattern": "\\bmongosh.*dropDatabase",
18
+ "reason": "MongoDB dropDatabase",
19
+ "id": "databases.mongodb-dropdatabase",
20
+ "anchored": false
21
+ },
22
+ {
23
+ "pattern": "\\bmongo.*dropDatabase",
24
+ "reason": "MongoDB dropDatabase",
25
+ "id": "databases.mongodb-dropdatabase-2",
26
+ "anchored": false
27
+ },
28
+ {
29
+ "pattern": "DELETE\\s+FROM\\s+\\w+\\s*;",
30
+ "reason": "DELETE without WHERE clause (will delete ALL rows)",
31
+ "id": "databases.delete-without-where-clause-will-delete-all-rows",
32
+ "anchored": false
33
+ },
34
+ {
35
+ "pattern": "DELETE\\s+FROM\\s+\\w+\\s*$",
36
+ "reason": "DELETE without WHERE clause (will delete ALL rows)",
37
+ "id": "databases.delete-without-where-clause-will-delete-all-rows-2",
38
+ "anchored": false
39
+ },
40
+ {
41
+ "pattern": "DELETE\\s+\\*\\s+FROM",
42
+ "reason": "DELETE * (will delete ALL rows)",
43
+ "id": "databases.delete-will-delete-all-rows",
44
+ "anchored": false
45
+ },
46
+ {
47
+ "pattern": "\\bTRUNCATE\\s+TABLE\\b",
48
+ "reason": "TRUNCATE TABLE (will delete ALL rows)",
49
+ "id": "databases.truncate-table-will-delete-all-rows",
50
+ "anchored": false
51
+ },
52
+ {
53
+ "pattern": "\\bDROP\\s+TABLE\\b",
54
+ "reason": "DROP TABLE",
55
+ "id": "databases.drop-table",
56
+ "anchored": false
57
+ },
58
+ {
59
+ "pattern": "\\bDROP\\s+DATABASE\\b",
60
+ "reason": "DROP DATABASE",
61
+ "id": "databases.drop-database",
62
+ "anchored": false
63
+ },
64
+ {
65
+ "pattern": "\\bDELETE\\s+FROM\\s+\\w+\\s+WHERE\\b.*\\bid\\s*=",
66
+ "reason": "SQL DELETE with specific ID",
67
+ "ask": true,
68
+ "id": "databases.sql-delete-with-specific-id",
69
+ "anchored": false
70
+ },
71
+ {
72
+ "pattern": "\\bpsql\\b.*\\b(INSERT\\s+INTO|UPDATE\\s+\\w+\\s+SET|ALTER\\s+TABLE|CREATE\\s+TABLE|GRANT\\s+|REVOKE\\s+)",
73
+ "reason": "psql executing a write statement (INSERT/UPDATE/ALTER/CREATE/GRANT)",
74
+ "ask": true,
75
+ "id": "db.psql-write",
76
+ "anchored": false
77
+ },
78
+ {
79
+ "pattern": "\\b(mysql|mariadb)\\b.*\\b(INSERT\\s+INTO|UPDATE\\s+\\w+\\s+SET|ALTER\\s+TABLE|CREATE\\s+TABLE|GRANT\\s+|REVOKE\\s+)",
80
+ "reason": "mysql executing a write statement (INSERT/UPDATE/ALTER/CREATE/GRANT)",
81
+ "ask": true,
82
+ "id": "db.mysql-write",
83
+ "anchored": false
84
+ },
85
+ {
86
+ "pattern": "\\bmongosh?\\b.*\\.(insertOne|insertMany|updateOne|updateMany|deleteOne|deleteMany|replaceOne|bulkWrite)\\b",
87
+ "reason": "mongosh executing a write operation (insert/update/delete)",
88
+ "ask": true,
89
+ "id": "db.mongosh-write",
90
+ "anchored": false
91
+ },
92
+ {
93
+ "pattern": "\\bdocker\\s+rm\\s+.*-f.*\\$\\(docker\\s+ps",
94
+ "reason": "docker rm -f $(docker ps) (force removes containers)",
95
+ "id": "containers.docker-rm-f-docker-ps-force-removes-containers",
96
+ "anchored": false
97
+ },
98
+ {
99
+ "pattern": "\\bgit\\b[^;&|]*\\s-c\\s*(?:alias\\.[^.=\\s]+=!|(?:core\\.(?:sshcommand|fsmonitor|pager|editor|askpass|alternaterefscommand)|sequence\\.editor|gpg\\.program|diff\\.external|credential\\.helper|uploadpack\\.packobjectshook|filter\\.[^.=\\s]+\\.(?:clean|smudge|process)|difftool\\.[^.=\\s]+\\.cmd|mergetool\\.[^.=\\s]+\\.cmd|merge\\.[^.=\\s]+\\.driver|remote\\.[^.=\\s]+\\.(?:uploadpack|receivepack)|browser\\.[^.=\\s]+\\.cmd|man\\.[^.=\\s]+\\.cmd)=)",
100
+ "reason": "git -c sets a config key that names a program git will execute",
101
+ "ask": true,
102
+ "id": "git.config-exec-key",
103
+ "anchored": false
104
+ },
105
+ {
106
+ "pattern": "(?:^|[;&|(`]|\\$\\(|(?:\\s-c|\\bssh\\s+\\S+)\\s+['\"])\\s*(?:\\w+=\\S*\\s+|(?:sudo|doas|env|nohup|time|command|exec|timeout|gtimeout|nice|xargs|stdbuf|ionice|setsid|caffeinate|unbuffer)\\s+(?:-\\S+\\s+|\\d+[smhd]?\\s+)*)*git\\b[^;&|]*?\\sconfig\\b(?![^;&|]*\\s(?:--(?:get|unset|list|edit)|-l\\b|-e\\b))[^;&|]*?\\s(?:core\\.(?:sshcommand|fsmonitor|pager|editor|askpass|alternaterefscommand)|sequence\\.editor|gpg\\.program|diff\\.external|credential\\.helper|uploadpack\\.packobjectshook|filter\\.[^.\\s]+\\.(?:clean|smudge|process)|difftool\\.[^.\\s]+\\.cmd|mergetool\\.[^.\\s]+\\.cmd|merge\\.[^.\\s]+\\.driver|remote\\.[^.\\s]+\\.(?:uploadpack|receivepack)|browser\\.[^.\\s]+\\.cmd|man\\.[^.\\s]+\\.cmd)\\s+(?!(?:(?:vim|nvim|vi|view|nano|pico|micro|emacs|ed|ex|less|more|cat|bat|most|true|false|code|subl|mate|zed)(?:\\s+-{1,2}[A-Za-z][\\w-]*(?:=[\\w.-]+)?)*(?=(?:\\s+--(?:global|local|system|worktree|replace-all|add))*\\s*(?:$|[;&|)`]))|(['\\\"])(?:vim|nvim|vi|view|nano|pico|micro|emacs|ed|ex|less|more|cat|bat|most|true|false|code|subl|mate|zed)(?:\\s+-{1,2}[A-Za-z][\\w-]*(?:=[\\w.-]+)?)*\\1(?=(?:\\s+--(?:global|local|system|worktree|replace-all|add))*\\s*(?:$|[;&|)`]))))[^\\s\\x00]",
107
+ "reason": "git config sets a key that names a program git will execute",
108
+ "ask": true,
109
+ "id": "git.config-set-exec-key",
110
+ "anchored": false
111
+ },
112
+ {
113
+ "pattern": "\\bgit\\s+config\\b(?![^;&|]*\\s(?:--(?:get|unset|list|edit)|-l\\b|-e\\b))[^;&|]*?\\salias\\.[^.=\\s]+\\s+['\"]?!",
114
+ "reason": "git config sets a shell-command alias (alias.x = !cmd)",
115
+ "ask": true,
116
+ "id": "git.config-set-alias-bang",
117
+ "anchored": false
118
+ }
119
+ ]
120
+ }
@@ -0,0 +1,155 @@
1
+ # Wrapped-payload damage control rules
2
+ #
3
+ # Every rule here matches text that arrives INSIDE another command's quoted
4
+ # argument -- a SQL statement handed to psql, a script handed to python -c /
5
+ # perl -e / mongosh --eval, a $(...) substitution. They are the rules that fail
6
+ # the anchoring shape test, so they live together in one uniformly UNANCHORED
7
+ # file (#915).
8
+ #
9
+ # WHY A FILE AND NOT A PER-RULE FLAG. `anchored: true` swaps the haystack for
10
+ # masked_subcommands(), which blanks every fully-quoted token containing
11
+ # whitespace regardless of position. That is lossless for a command-prefix rule
12
+ # and fatal here: masking removes exactly the text these rules exist to catch.
13
+ # Anchoring is therefore decided per FILE, gated on the git.yaml shape test --
14
+ # every rule command-prefix shaped AND the tool takes no inner-command payload
15
+ # -- so there is no per-rule skip-list to keep in sync (see
16
+ # ~/.claude/rules/architecture.md). remote.yaml is the other unanchored file:
17
+ # ssh's payload is the same shape, and its rules are kept whole there because
18
+ # they double-cover the local rules in core.yaml.
19
+ #
20
+ # Each rule carries an explicit `id:` pinned to the id it had before the move,
21
+ # so `safety.disabled_rules` / `unattended_allow` entries keep working.
22
+ #
23
+ # DO NOT add `anchored: true` to anything in this file. The invariant is
24
+ # enforced by tests/unit/test_damage_control_payload_anchoring.py.
25
+
26
+ bashToolPatterns:
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # INTERPRETER / CLIENT EVAL PAYLOADS
30
+ # ---------------------------------------------------------------------------
31
+
32
+ - pattern: 'shutil\.rmtree'
33
+ reason: shutil.rmtree recursive delete (equivalent to rm -rf)
34
+ id: core.shutil-rmtree-recursive-delete-equivalent-to-rm-rf
35
+ anchored: false
36
+
37
+ - pattern: '\bperl\b.*\bunlink\b'
38
+ reason: perl unlink delete (use git clean or manual cleanup)
39
+ bypassable: true
40
+ id: core.perl-unlink-delete-use-git-clean-or-manual-cleanup
41
+ anchored: false
42
+
43
+ - pattern: '\bmongosh.*dropDatabase'
44
+ reason: MongoDB dropDatabase
45
+ id: databases.mongodb-dropdatabase
46
+ anchored: false
47
+
48
+ - pattern: '\bmongo.*dropDatabase'
49
+ reason: MongoDB dropDatabase
50
+ id: databases.mongodb-dropdatabase-2
51
+ anchored: false
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # SQL STATEMENTS
55
+ # ---------------------------------------------------------------------------
56
+
57
+ - pattern: 'DELETE\s+FROM\s+\w+\s*;'
58
+ reason: DELETE without WHERE clause (will delete ALL rows)
59
+ id: databases.delete-without-where-clause-will-delete-all-rows
60
+ anchored: false
61
+
62
+ - pattern: 'DELETE\s+FROM\s+\w+\s*$'
63
+ reason: DELETE without WHERE clause (will delete ALL rows)
64
+ id: databases.delete-without-where-clause-will-delete-all-rows-2
65
+ anchored: false
66
+
67
+ - pattern: 'DELETE\s+\*\s+FROM'
68
+ reason: DELETE * (will delete ALL rows)
69
+ id: databases.delete-will-delete-all-rows
70
+ anchored: false
71
+
72
+ - pattern: '\bTRUNCATE\s+TABLE\b'
73
+ reason: TRUNCATE TABLE (will delete ALL rows)
74
+ id: databases.truncate-table-will-delete-all-rows
75
+ anchored: false
76
+
77
+ - pattern: '\bDROP\s+TABLE\b'
78
+ reason: DROP TABLE
79
+ id: databases.drop-table
80
+ anchored: false
81
+
82
+ - pattern: '\bDROP\s+DATABASE\b'
83
+ reason: DROP DATABASE
84
+ id: databases.drop-database
85
+ anchored: false
86
+
87
+ - pattern: '\bDELETE\s+FROM\s+\w+\s+WHERE\b.*\bid\s*='
88
+ reason: SQL DELETE with specific ID
89
+ ask: true
90
+ id: databases.sql-delete-with-specific-id
91
+ anchored: false
92
+
93
+ - pattern: '\bpsql\b.*\b(INSERT\s+INTO|UPDATE\s+\w+\s+SET|ALTER\s+TABLE|CREATE\s+TABLE|GRANT\s+|REVOKE\s+)'
94
+ reason: psql executing a write statement (INSERT/UPDATE/ALTER/CREATE/GRANT)
95
+ ask: true
96
+ id: db.psql-write
97
+ anchored: false
98
+
99
+ - pattern: '\b(mysql|mariadb)\b.*\b(INSERT\s+INTO|UPDATE\s+\w+\s+SET|ALTER\s+TABLE|CREATE\s+TABLE|GRANT\s+|REVOKE\s+)'
100
+ reason: mysql executing a write statement (INSERT/UPDATE/ALTER/CREATE/GRANT)
101
+ ask: true
102
+ id: db.mysql-write
103
+ anchored: false
104
+
105
+ - pattern: '\bmongosh?\b.*\.(insertOne|insertMany|updateOne|updateMany|deleteOne|deleteMany|replaceOne|bulkWrite)\b'
106
+ reason: mongosh executing a write operation (insert/update/delete)
107
+ ask: true
108
+ id: db.mongosh-write
109
+ anchored: false
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # COMMAND SUBSTITUTION
113
+ # ---------------------------------------------------------------------------
114
+
115
+ - pattern: '\bdocker\s+rm\s+.*-f.*\$\(docker\s+ps'
116
+ reason: docker rm -f $(docker ps) (force removes containers)
117
+ id: containers.docker-rm-f-docker-ps-force-removes-containers
118
+ anchored: false
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # GIT CONFIG KEYS THAT NAME AN EXECUTABLE (#921)
122
+ # ---------------------------------------------------------------------------
123
+ # A subset of `git -c` keys name a program git will RUN (core.sshCommand,
124
+ # core.fsmonitor, core.pager, ...). The value is additionally re-scanned as a
125
+ # command in its own right (`_git_exec_config_payloads`), so a dangerous
126
+ # payload hits its own block rule; this rule is the confirm tier for
127
+ # payloads no rule recognizes. Unanchored: the key sits in an option's
128
+ # attached/separate value, never at command position.
129
+
130
+ - pattern: '\bgit\b[^;&|]*\s-c\s*(?:alias\.[^.=\s]+=!|(?:core\.(?:sshcommand|fsmonitor|pager|editor|askpass|alternaterefscommand)|sequence\.editor|gpg\.program|diff\.external|credential\.helper|uploadpack\.packobjectshook|filter\.[^.=\s]+\.(?:clean|smudge|process)|difftool\.[^.=\s]+\.cmd|mergetool\.[^.=\s]+\.cmd|merge\.[^.=\s]+\.driver|remote\.[^.=\s]+\.(?:uploadpack|receivepack)|browser\.[^.=\s]+\.cmd|man\.[^.=\s]+\.cmd)=)'
131
+ reason: git -c sets a config key that names a program git will execute
132
+ ask: true
133
+ id: git.config-exec-key
134
+ anchored: false
135
+
136
+ # Set-form `git config <exec-key> <value>` (#1133): git runs the value through
137
+ # a shell, so only a known editor/pager word is bare-safe -- either bare, or
138
+ # quoted with plain flags (`'vim -f'`, `'code --wait'`). Anything else asks:
139
+ # a separator or substitution inside quotes (`'less;sh'`), an escape
140
+ # (`less\;sh`), `$'..'`, an unknown program. Unanchored: the quoting and
141
+ # escaping that decide safety are invisible in the masked view. Command
142
+ # position is required, so a message that merely quotes the command passes.
143
+ - pattern: '(?:^|[;&|(`]|\$\(|(?:\s-c|\bssh\s+\S+)\s+[''"])\s*(?:\w+=\S*\s+|(?:sudo|doas|env|nohup|time|command|exec|timeout|gtimeout|nice|xargs|stdbuf|ionice|setsid|caffeinate|unbuffer)\s+(?:-\S+\s+|\d+[smhd]?\s+)*)*git\b[^;&|]*?\sconfig\b(?![^;&|]*\s(?:--(?:get|unset|list|edit)|-l\b|-e\b))[^;&|]*?\s(?:core\.(?:sshcommand|fsmonitor|pager|editor|askpass|alternaterefscommand)|sequence\.editor|gpg\.program|diff\.external|credential\.helper|uploadpack\.packobjectshook|filter\.[^.\s]+\.(?:clean|smudge|process)|difftool\.[^.\s]+\.cmd|mergetool\.[^.\s]+\.cmd|merge\.[^.\s]+\.driver|remote\.[^.\s]+\.(?:uploadpack|receivepack)|browser\.[^.\s]+\.cmd|man\.[^.\s]+\.cmd)\s+(?!(?:(?:vim|nvim|vi|view|nano|pico|micro|emacs|ed|ex|less|more|cat|bat|most|true|false|code|subl|mate|zed)(?:\s+-{1,2}[A-Za-z][\w-]*(?:=[\w.-]+)?)*(?=(?:\s+--(?:global|local|system|worktree|replace-all|add))*\s*(?:$|[;&|)`]))|([''\"])(?:vim|nvim|vi|view|nano|pico|micro|emacs|ed|ex|less|more|cat|bat|most|true|false|code|subl|mate|zed)(?:\s+-{1,2}[A-Za-z][\w-]*(?:=[\w.-]+)?)*\1(?=(?:\s+--(?:global|local|system|worktree|replace-all|add))*\s*(?:$|[;&|)`]))))[^\s\x00]'
144
+ reason: git config sets a key that names a program git will execute
145
+ ask: true
146
+ id: git.config-set-exec-key
147
+ anchored: false
148
+
149
+ # Set-form alias with the bang form (`git config alias.x '!cmd'`): the `!` sits
150
+ # inside a quoted value, which the anchored git.yaml rules see masked.
151
+ - pattern: '\bgit\s+config\b(?![^;&|]*\s(?:--(?:get|unset|list|edit)|-l\b|-e\b))[^;&|]*?\salias\.[^.=\s]+\s+[''"]?!'
152
+ reason: git config sets a shell-command alias (alias.x = !cmd)
153
+ ask: true
154
+ id: git.config-set-alias-bang
155
+ anchored: false
@@ -0,0 +1,46 @@
1
+ {
2
+ "bashToolPatterns": [
3
+ {
4
+ "pattern": "\\bcargo\\s+publish\\b",
5
+ "reason": "cargo publish (publishes a crate to crates.io)",
6
+ "ask": true,
7
+ "id": "publish.cargo",
8
+ "anchored": true
9
+ },
10
+ {
11
+ "pattern": "\\bpoetry\\s+publish\\b",
12
+ "reason": "poetry publish (publishes a package to PyPI)",
13
+ "ask": true,
14
+ "id": "publish.poetry",
15
+ "anchored": true
16
+ },
17
+ {
18
+ "pattern": "\\btwine\\s+upload\\b",
19
+ "reason": "twine upload (publishes a package to PyPI)",
20
+ "ask": true,
21
+ "id": "publish.twine",
22
+ "anchored": true
23
+ },
24
+ {
25
+ "pattern": "\\b(pnpm|yarn)\\s+publish\\b",
26
+ "reason": "pnpm/yarn publish (publishes a package to npm registry)",
27
+ "ask": true,
28
+ "id": "publish.pnpm-yarn",
29
+ "anchored": true
30
+ },
31
+ {
32
+ "pattern": "\\bgem\\s+push\\b",
33
+ "reason": "gem push (publishes a gem to RubyGems)",
34
+ "ask": true,
35
+ "id": "publish.gem",
36
+ "anchored": true
37
+ },
38
+ {
39
+ "pattern": "\\bmvn\\s+(deploy|.*\\bdeploy:deploy)\\b",
40
+ "reason": "mvn deploy (publishes artifacts to a Maven repository)",
41
+ "ask": true,
42
+ "id": "publish.maven",
43
+ "anchored": true
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,46 @@
1
+ # Package publish damage control rules
2
+ #
3
+ # Publishing to a public registry is irreversible and outward-facing (downstream
4
+ # users pull what you push), so it must be caught in UNATTENDED sessions. All
5
+ # `ask: true` — see outbound.yaml for how `ask` resolves per session mode.
6
+ #
7
+ # Note: `npm publish` and `uv publish` are already `ask`-tier via the write-access
8
+ # tooldef commands (agentwire/tooldefs/{npm,uv}.yaml). These rules cover the
9
+ # registries that have no tooldef.
10
+
11
+ bashToolPatterns:
12
+ - pattern: '\bcargo\s+publish\b'
13
+ reason: cargo publish (publishes a crate to crates.io)
14
+ ask: true
15
+ id: publish.cargo
16
+ anchored: true
17
+
18
+ - pattern: '\bpoetry\s+publish\b'
19
+ reason: poetry publish (publishes a package to PyPI)
20
+ ask: true
21
+ id: publish.poetry
22
+ anchored: true
23
+
24
+ - pattern: '\btwine\s+upload\b'
25
+ reason: twine upload (publishes a package to PyPI)
26
+ ask: true
27
+ id: publish.twine
28
+ anchored: true
29
+
30
+ - pattern: '\b(pnpm|yarn)\s+publish\b'
31
+ reason: pnpm/yarn publish (publishes a package to npm registry)
32
+ ask: true
33
+ id: publish.pnpm-yarn
34
+ anchored: true
35
+
36
+ - pattern: '\bgem\s+push\b'
37
+ reason: gem push (publishes a gem to RubyGems)
38
+ ask: true
39
+ id: publish.gem
40
+ anchored: true
41
+
42
+ - pattern: '\bmvn\s+(deploy|.*\bdeploy:deploy)\b'
43
+ reason: mvn deploy (publishes artifacts to a Maven repository)
44
+ ask: true
45
+ id: publish.maven
46
+ anchored: true
@@ -0,0 +1,10 @@
1
+ {
2
+ "bashToolPatterns": [
3
+ {
4
+ "id": "registry.npm-unpublish-removes-package-from-registry",
5
+ "pattern": "\\bnpm\\s+unpublish\\b",
6
+ "reason": "npm unpublish (removes package from registry)",
7
+ "anchored": true
8
+ }
9
+ ]
10
+ }
@@ -0,0 +1,11 @@
1
+ # Package registry damage control (default-on group `registry`, #1141)
2
+ #
3
+ # Vendor-neutral registry operations that must stay protected on a fresh install,
4
+ # independent of whether the `publish` pack is enabled. `npm publish` itself is
5
+ # ask-tier through the npm tooldef, which belongs to this same group.
6
+
7
+ bashToolPatterns:
8
+ - id: registry.npm-unpublish-removes-package-from-registry
9
+ pattern: '\bnpm\s+unpublish\b'
10
+ reason: npm unpublish (removes package from registry)
11
+ anchored: true
@@ -0,0 +1,31 @@
1
+ {
2
+ "bashToolPatterns": [
3
+ {
4
+ "id": "remote.ssh-remote-reboot",
5
+ "pattern": "\\bssh\\s+.*\\breboot\\b",
6
+ "reason": "SSH remote reboot",
7
+ "ask": true,
8
+ "anchored": false
9
+ },
10
+ {
11
+ "id": "remote.ssh-remote-shutdown",
12
+ "pattern": "\\bssh\\s+.*\\bshutdown\\b",
13
+ "reason": "SSH remote shutdown",
14
+ "ask": true,
15
+ "anchored": false
16
+ },
17
+ {
18
+ "id": "remote.ssh-remote-service-stop",
19
+ "pattern": "\\bssh\\s+.*\\bsystemctl\\s+stop\\b",
20
+ "reason": "SSH remote service stop",
21
+ "ask": true,
22
+ "anchored": false
23
+ },
24
+ {
25
+ "id": "remote.ssh-remote-docker-rm-f",
26
+ "pattern": "\\bssh\\s+.*\\bdocker\\s+rm\\s+.*-f",
27
+ "reason": "SSH remote docker rm -f",
28
+ "anchored": false
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,53 @@
1
+ # Remote execution damage control rules
2
+ # SSH remote execution safeguards
3
+ #
4
+ # MOSTLY SUPERSEDED BY THE WRAPPER-PAYLOAD RESCAN (#924). ssh's remote command
5
+ # is now extracted and re-scanned as a command in its own right
6
+ # (`_ssh_remote_payload` in agentwire/safety/_core.py), so EVERY rule —
7
+ # anchored ones included, at real command positions — applies to the wrapped
8
+ # form automatically. The hand-written ssh twins of local rules (rm -rf,
9
+ # sudo rm, mkfs, dd-to-device, dropdb, mysqladmin drop, redis FLUSHALL,
10
+ # docker system prune) were measured redundant against the full corpus and
11
+ # deleted; do not add new twins here — a rule written once applies over ssh.
12
+ #
13
+ # What remains is the ssh-ONLY surface: operations with no local rule twin
14
+ # (rebooting/stopping a REMOTE machine is a risk locally unruled), plus one
15
+ # rule that is deliberately STRICTER than its local tooldef tier.
16
+ #
17
+ # WHY THIS FILE STAYS UNANCHORED (#915)
18
+ #
19
+ # The dangerous text is ssh's quoted payload; `anchored: true` matches against
20
+ # masked_subcommands(), which blanks any fully-quoted token containing
21
+ # whitespace -- so anchoring would blank exactly the half these rules read.
22
+ #
23
+ # DO NOT add `anchored: true` to anything in this file. Enforced by
24
+ # tests/unit/test_damage_control_payload_anchoring.py.
25
+
26
+ bashToolPatterns:
27
+ # Remote machine-state operations — no local rule twin exists for these, so
28
+ # the rescan alone leaves them uncovered.
29
+ - id: remote.ssh-remote-reboot
30
+ pattern: '\bssh\s+.*\breboot\b'
31
+ reason: SSH remote reboot
32
+ ask: true
33
+ anchored: false
34
+
35
+ - id: remote.ssh-remote-shutdown
36
+ pattern: '\bssh\s+.*\bshutdown\b'
37
+ reason: SSH remote shutdown
38
+ ask: true
39
+ anchored: false
40
+
41
+ - id: remote.ssh-remote-service-stop
42
+ pattern: '\bssh\s+.*\bsystemctl\s+stop\b'
43
+ reason: SSH remote service stop
44
+ ask: true
45
+ anchored: false
46
+
47
+ # Deliberately STRICTER than the local tier: `docker rm -f` locally is
48
+ # ask-tier (tooldef); on a remote box it stays a hard block — deleting this
49
+ # twin would downgrade the wrapped form from block to ask.
50
+ - id: remote.ssh-remote-docker-rm-f
51
+ pattern: '\bssh\s+.*\bdocker\s+rm\s+.*-f'
52
+ reason: SSH remote docker rm -f
53
+ anchored: false
@@ -0,0 +1,17 @@
1
+ {
2
+ "bashToolPatterns": [
3
+ {
4
+ "id": "agentwire.tmux-kill-server-destroys-all-tmux-sessions-including-agentw",
5
+ "pattern": "\\btmux\\s+kill-server\\b",
6
+ "reason": "tmux kill-server (destroys ALL tmux sessions)",
7
+ "anchored": true
8
+ },
9
+ {
10
+ "id": "agentwire.tmux-kill-session-a-kills-all-except-current",
11
+ "pattern": "\\btmux\\s+kill-session\\s+.*-a\\b",
12
+ "reason": "tmux kill-session -a (kills all except current)",
13
+ "ask": true,
14
+ "anchored": true
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,16 @@
1
+ # tmux hazards (opt-in pack `tmux`)
2
+ #
3
+ # Destroying every tmux session at once is a hazard for anyone who keeps
4
+ # long-running work in tmux, agent or not.
5
+
6
+ bashToolPatterns:
7
+ - id: agentwire.tmux-kill-server-destroys-all-tmux-sessions-including-agentw
8
+ pattern: '\btmux\s+kill-server\b'
9
+ reason: tmux kill-server (destroys ALL tmux sessions)
10
+ anchored: true
11
+
12
+ - id: agentwire.tmux-kill-session-a-kills-all-except-current
13
+ pattern: '\btmux\s+kill-session\s+.*-a\b'
14
+ reason: tmux kill-session -a (kills all except current)
15
+ ask: true
16
+ anchored: true
@@ -0,0 +1,94 @@
1
+ """The enforced rule set as ``{stable id: "tier:fingerprint"}``, and the diff between two of them (#1209 M5).
2
+
3
+ ``install.json`` records one of these at every acknowledged install. ``status`` compares the
4
+ current set against it ("ruleset changed since last acknowledged") and ``init --refresh``
5
+ prints the diff by id. A refresh that WIDENS what is permitted must say so (#916): a removed
6
+ rule, a lowered tier, a new allow-path and a new default unattended grant are marked ``!!``.
7
+
8
+ Standard library only.
9
+ """
10
+
11
+ import hashlib
12
+ import json
13
+ from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Tuple
14
+
15
+ TIER_RANK = {"block": 3, "ask": 2, "allow": 1}
16
+ _PATH_LISTS = (("zeroAccessPaths", "block"), ("readOnlyPaths", "block"), ("noDeletePaths", "block"))
17
+ ALLOW_NAMESPACE = "allowedPaths:"
18
+
19
+
20
+ class Change(NamedTuple):
21
+ loosening: bool
22
+ text: str
23
+
24
+
25
+ def _fingerprint(value: Any) -> str:
26
+ return hashlib.sha256(json.dumps(value, sort_keys=True, default=str).encode()).hexdigest()[:10]
27
+
28
+
29
+ def _label(item: Any) -> str:
30
+ if isinstance(item, str):
31
+ return item
32
+ if isinstance(item, dict):
33
+ for key in ("path", "pattern", "entry", "id"):
34
+ if isinstance(item.get(key), str):
35
+ return item[key]
36
+ return json.dumps(item, sort_keys=True, default=str)
37
+
38
+
39
+ def digest(config: Dict[str, Any]) -> Dict[str, str]:
40
+ """Every enforced rule of ``config`` (an effective config) keyed by its stable id."""
41
+ out: Dict[str, str] = {}
42
+ for rule in config.get("bashToolPatterns") or []:
43
+ if isinstance(rule, dict) and rule.get("id"):
44
+ out.setdefault(rule["id"], f"{'ask' if rule.get('ask') else 'block'}:{_fingerprint(rule.get('pattern'))}")
45
+ for policy in config.get("mcpToolPolicies") or []:
46
+ if isinstance(policy, dict) and policy.get("id"):
47
+ match = policy.get("match") or policy.get("tool") or policy.get("pattern")
48
+ out.setdefault(policy["id"], f"{policy.get('tier', 'allow')}:{_fingerprint(match)}")
49
+ for key, tier in _PATH_LISTS:
50
+ for item in config.get(key) or []:
51
+ out.setdefault(f"{key}:{_label(item)}", f"{tier}:-")
52
+ for item in config.get("allowedPaths") or []:
53
+ out.setdefault(f"{ALLOW_NAMESPACE}{_label(item)}", "allow:-")
54
+ return out
55
+
56
+
57
+ def _tier(value: Optional[str]) -> str:
58
+ return (value or "").split(":", 1)[0]
59
+
60
+
61
+ def diff(old: Dict[str, str], new: Dict[str, str],
62
+ old_grants: Iterable[str] = (), new_grants: Iterable[str] = ()) -> List[Change]:
63
+ """Added / removed / changed ids between two digests, plus new default grants. Loosening is flagged."""
64
+ changes: List[Change] = []
65
+ for rid in sorted(set(old) | set(new)):
66
+ before, after = old.get(rid), new.get(rid)
67
+ if before == after:
68
+ continue
69
+ allow_entry = rid.startswith(ALLOW_NAMESPACE)
70
+ if before is None:
71
+ changes.append(Change(allow_entry, f"+ {rid} ({_tier(after)})" + (" widens what is allowed" if allow_entry else "")))
72
+ elif after is None:
73
+ changes.append(Change(not allow_entry, f"- {rid} (was {_tier(before)})"))
74
+ elif _tier(before) != _tier(after):
75
+ lowered = TIER_RANK.get(_tier(after), 0) < TIER_RANK.get(_tier(before), 0)
76
+ changes.append(Change(lowered, f"~ {rid} {_tier(before)} -> {_tier(after)}"))
77
+ else:
78
+ changes.append(Change(False, f"~ {rid} pattern changed (review: a narrower pattern permits more)"))
79
+ for grant in sorted(set(new_grants) - set(old_grants)):
80
+ changes.append(Change(True, f"+ default unattended grant {grant}"))
81
+ return changes
82
+
83
+
84
+ def render(changes: List[Change]) -> List[str]:
85
+ return [f"{'!!' if c.loosening else ' '} {c.text}" for c in changes]
86
+
87
+
88
+ def has_loosening(changes: List[Change]) -> bool:
89
+ return any(c.loosening for c in changes)
90
+
91
+
92
+ def summary(changes: List[Change]) -> Tuple[int, int]:
93
+ """``(total changes, loosening changes)``."""
94
+ return len(changes), sum(1 for c in changes if c.loosening)