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,177 @@
1
+ """The host-supplied half of the damage-control engine (#1209 M3, section 4.2).
2
+
3
+ The engine knows what is dangerous in general; a HOST (an application that embeds it)
4
+ adds what is dangerous *around itself*: its own config and control-plane files, the
5
+ commands that must never run through an agent, the env vars its scheduler stamps, the
6
+ rule ids it is willing to grant unattended. A ``Profile`` is that whole surface, as
7
+ plain data, so the engine has no host names in its logic.
8
+
9
+ ``Profile`` only ever ADDS. The generic control plane (the project ``.damagecontrol.yml``,
10
+ ``~/.claude/settings.json``, ``~/.claude/hooks/*``, the host's policy file) is a kernel
11
+ that ``install_profile`` (in ``_core.py``) puts first and a profile cannot remove or
12
+ reorder: the control-plane and protected-command checks run before the kill switch and
13
+ nothing a profile supplies can loosen them.
14
+
15
+ A profile reaches a hook as DATA: the host's installer writes it with ``dump_profile`` to a
16
+ protected file and the hook's command line names it (``--profile``), so the hook never imports
17
+ the host. Standard library only; plain ``NamedTuple``s rather than dataclasses.
18
+ """
19
+
20
+ import json
21
+ from pathlib import Path
22
+ from typing import Any, Dict, NamedTuple, Tuple
23
+
24
+
25
+ class GroupSpec(NamedTuple):
26
+ """One rule group or opt-in pack: which bundled files it owns (#1141)."""
27
+
28
+ name: str
29
+ kind: str # "group" | "pack"
30
+ description: str
31
+ rules: Tuple[str, ...] = ()
32
+ tooldefs: Tuple[str, ...] = ()
33
+ kernel: bool = False
34
+ binaries: Tuple[str, ...] = () # vendor CLIs whose presence suggests a pack
35
+ # Rule files shipped by a host layer (``Profile.extra_rules_dirs``). Owned by the
36
+ # SAME entry as the generic file they were split from, so an opt-out or kernel pin
37
+ # still covers every entry that group covered before the split.
38
+ layer_rules: Tuple[str, ...] = ()
39
+
40
+ def files(self, section: str) -> Tuple[str, ...]:
41
+ """Every bundled file this entry owns in ``section`` ("rules" | "tooldefs")."""
42
+ return self.rules + self.layer_rules if section == "rules" else self.tooldefs
43
+
44
+
45
+ class McpCommandTemplate(NamedTuple):
46
+ """How an outbound MCP tool maps onto the shell command it runs under the hood.
47
+
48
+ The synthesized command must match the same rule the Bash shell-out matches. The
49
+ body is omitted on purpose (privacy, audit clarity): rules key on the verb.
50
+ ``argv`` is the fixed prefix; each ``flags`` entry is ``(tool_input key, flag, repeat)``:
51
+ an empty value omits the flag; ``repeat`` lets a list value emit the flag once per item,
52
+ otherwise the value is rendered as one ``str()``.
53
+ """
54
+
55
+ tool: str # short tool name, e.g. "email_send" (the part after the last "__")
56
+ argv: Tuple[str, ...]
57
+ flags: Tuple[Tuple[str, str, bool], ...] = ()
58
+
59
+
60
+ class AuditSpec(NamedTuple):
61
+ """How the host's sessions are named in an audit row, and where the rows live."""
62
+
63
+ # Under the config home (``Profile.config_dir``).
64
+ log_dir: str = "logs/damage-control"
65
+ # Env vars the host stamps on a session; empty = the host has none.
66
+ session_env: str = ""
67
+ agent_env: str = ""
68
+ # Directory under the config home of ``<session name>/metadata.json`` records, each
69
+ # carrying a ``conversation_ids`` chain; the entry naming the hook's conversation id
70
+ # names the session. Empty = the host keeps no such records.
71
+ sessions_dir: str = ""
72
+
73
+
74
+ class Profile(NamedTuple):
75
+ """Everything a host adds to the generic engine. Defaults are the generic engine."""
76
+
77
+ name: str = "generic"
78
+
79
+ # --- config home ------------------------------------------------------
80
+ # Where the host keeps its protected policy file (``damagecontrol.yml``) and its
81
+ # rule/tooldef overrides. ``config_dir_env`` (if set in the environment) relocates
82
+ # the override dirs only — never the policy file, which is always protected.
83
+ config_dir: str = "~/.damage-control"
84
+ config_dir_env: str = "DAMAGE_CONTROL_HOME"
85
+
86
+ # --- protected control plane (additive) -------------------------------
87
+ protected_paths: Tuple[str, ...] = ()
88
+ # Names a program may use for a control-plane location it builds at run time.
89
+ control_plane_words: Tuple[str, ...] = ()
90
+ # Extra regex alternatives for directories agents write by design (matched whole-segment).
91
+ benign_path_patterns: Tuple[str, ...] = ()
92
+ # Fixed stand-in for ``$HOME`` when judging what a ``~``-anchored entry covers.
93
+ home_alias: str = "/home/damage-control-canonical"
94
+
95
+ # --- protected commands (additive; no allowlist, no escape hatch, no kill switch) ---
96
+ protected_commands: Tuple[Tuple[str, str], ...] = ()
97
+
98
+ # --- unattended / headless --------------------------------------------
99
+ # The mechanism and scope evaluation live in the engine; the host names the env vars
100
+ # its scheduler stamps and the rule ids it grants by default (the engine ships none).
101
+ unattended_env: str = "DAMAGE_CONTROL_UNATTENDED"
102
+ unattended_allow_env: str = "DAMAGE_CONTROL_UNATTENDED_ALLOW"
103
+ default_unattended_allow: Tuple[str, ...] = ()
104
+ # First half of the sentence telling a blocked unattended run how to get a grant;
105
+ # ``{rule_id}`` is filled in. Empty = say nothing.
106
+ unattended_grant_hint: str = ""
107
+ # argv run detached when an unattended action is blocked, with the event as JSON on stdin:
108
+ # ``{"event": "unattended_block", "reason", "rule_id", "command"}``. Empty = no notification.
109
+ unattended_block_notifier: Tuple[str, ...] = ()
110
+
111
+ # --- audit ------------------------------------------------------------
112
+ audit: AuditSpec = AuditSpec()
113
+
114
+ # --- extra rules ------------------------------------------------------
115
+ # Absolute directories of rule files (JSON, or YAML a human wrote) merged after the generic rules.
116
+ extra_rules_dirs: Tuple[str, ...] = ()
117
+
118
+ # --- MCP --------------------------------------------------------------
119
+ mcp_command_templates: Tuple[McpCommandTemplate, ...] = ()
120
+
121
+ # --- groups / packs ---------------------------------------------------
122
+ groups: Tuple[GroupSpec, ...] = ()
123
+ # ``{existing group name: layer rule files}`` the host adds to generic groups.
124
+ group_layer_rules: Tuple[Tuple[str, Tuple[str, ...]], ...] = ()
125
+ default_packs: Tuple[str, ...] = ()
126
+
127
+ def layer_rules_for(self, group: str) -> Tuple[str, ...]:
128
+ return dict(self.group_layer_rules).get(group, ())
129
+
130
+ def template_for(self, tool_name: str):
131
+ short = tool_name.split("__")[-1]
132
+ for t in self.mcp_command_templates:
133
+ if t.tool == short:
134
+ return t
135
+ return None
136
+
137
+
138
+ GENERIC = Profile()
139
+
140
+
141
+ class ProfileError(ValueError):
142
+ """A profile manifest that cannot be read as a profile. Never answered with the generic one."""
143
+
144
+
145
+ def dump_profile(profile: Profile) -> str:
146
+ """``profile`` as the JSON a hook loads (tuples become lists; fields keep their names)."""
147
+ return json.dumps(profile._asdict(), indent=1, ensure_ascii=False) + "\n"
148
+
149
+
150
+ def _tuple(value: Any) -> Any:
151
+ """A JSON list as the tuple the profile's fields are, recursively."""
152
+ return tuple(_tuple(v) for v in value) if isinstance(value, list) else value
153
+
154
+
155
+ def load_profile(path: Path) -> Profile:
156
+ """Read a manifest written by :func:`dump_profile`. Raises :class:`ProfileError` on anything off."""
157
+ try:
158
+ raw = json.loads(Path(path).read_text())
159
+ except (OSError, ValueError) as e:
160
+ raise ProfileError(f"cannot read profile {path}: {e}") from e
161
+ if not isinstance(raw, dict):
162
+ raise ProfileError(f"profile {path} is not a JSON object")
163
+ unknown = sorted(set(raw) - set(Profile._fields))
164
+ if unknown:
165
+ raise ProfileError(f"profile {path} has unknown fields: {unknown}")
166
+ fields: Dict[str, Any] = {k: _tuple(v) for k, v in raw.items()}
167
+ fields["audit"] = AuditSpec(*fields.get("audit", ()))
168
+ fields["groups"] = tuple(GroupSpec(*g) for g in fields.get("groups", ()))
169
+ fields["mcp_command_templates"] = tuple(
170
+ McpCommandTemplate(*t) for t in fields.get("mcp_command_templates", ()))
171
+ try:
172
+ return Profile(**fields)
173
+ except TypeError as e:
174
+ raise ProfileError(f"profile {path} is malformed: {e}") from e
175
+
176
+
177
+ __all__ = ["AuditSpec", "GENERIC", "GroupSpec", "McpCommandTemplate", "Profile", "ProfileError", "dump_profile", "load_profile"]
@@ -0,0 +1,85 @@
1
+ {
2
+ "bashToolPatterns": [
3
+ {
4
+ "id": "aws.aws-s3-rm-recursive-deletes-all-objects",
5
+ "pattern": "\\baws\\s+s3\\s+rm\\s+.*--recursive",
6
+ "reason": "aws s3 rm --recursive (deletes all objects)",
7
+ "anchored": true
8
+ },
9
+ {
10
+ "id": "aws.aws-s3-rb-force-force-removes-bucket",
11
+ "pattern": "\\baws\\s+s3\\s+rb\\s+.*--force",
12
+ "reason": "aws s3 rb --force (force removes bucket)",
13
+ "anchored": true
14
+ },
15
+ {
16
+ "id": "aws.aws-ec2-terminate-instances",
17
+ "pattern": "\\baws\\s+ec2\\s+terminate-instances\\b",
18
+ "reason": "aws ec2 terminate-instances",
19
+ "anchored": true
20
+ },
21
+ {
22
+ "id": "aws.aws-rds-delete-db-instance",
23
+ "pattern": "\\baws\\s+rds\\s+delete-db-instance\\b",
24
+ "reason": "aws rds delete-db-instance",
25
+ "anchored": true
26
+ },
27
+ {
28
+ "id": "aws.aws-cloudformation-delete-stack-deletes-infrastructure",
29
+ "pattern": "\\baws\\s+cloudformation\\s+delete-stack\\b",
30
+ "reason": "aws cloudformation delete-stack (deletes infrastructure)",
31
+ "anchored": true
32
+ },
33
+ {
34
+ "id": "aws.aws-dynamodb-delete-table",
35
+ "pattern": "\\baws\\s+dynamodb\\s+delete-table\\b",
36
+ "reason": "aws dynamodb delete-table",
37
+ "anchored": true
38
+ },
39
+ {
40
+ "id": "aws.aws-eks-delete-cluster",
41
+ "pattern": "\\baws\\s+eks\\s+delete-cluster\\b",
42
+ "reason": "aws eks delete-cluster",
43
+ "anchored": true
44
+ },
45
+ {
46
+ "id": "aws.aws-lambda-delete-function",
47
+ "pattern": "\\baws\\s+lambda\\s+delete-function\\b",
48
+ "reason": "aws lambda delete-function",
49
+ "anchored": true
50
+ },
51
+ {
52
+ "id": "aws.aws-iam-delete-role",
53
+ "pattern": "\\baws\\s+iam\\s+delete-role\\b",
54
+ "reason": "aws iam delete-role",
55
+ "anchored": true
56
+ },
57
+ {
58
+ "id": "aws.aws-iam-delete-user",
59
+ "pattern": "\\baws\\s+iam\\s+delete-user\\b",
60
+ "reason": "aws iam delete-user",
61
+ "anchored": true
62
+ },
63
+ {
64
+ "pattern": "\\baws\\s+cloudformation\\s+deploy\\b",
65
+ "reason": "aws cloudformation deploy (ships an infrastructure change)",
66
+ "ask": true,
67
+ "id": "deploy.aws-cloudformation",
68
+ "anchored": true
69
+ },
70
+ {
71
+ "pattern": "\\baws\\s+lambda\\s+update-function-code\\b",
72
+ "reason": "aws lambda update-function-code (ships new function code)",
73
+ "ask": true,
74
+ "id": "deploy.aws-lambda-update",
75
+ "anchored": true
76
+ },
77
+ {
78
+ "pattern": "\\baws\\s+ecs\\s+update-service\\b",
79
+ "reason": "aws ecs update-service (ships an ECS deployment)",
80
+ "ask": true,
81
+ "id": "deploy.aws-ecs-update",
82
+ "anchored": true
83
+ }
84
+ ]
85
+ }
@@ -0,0 +1,76 @@
1
+ # AWS CLI damage control rules
2
+
3
+ bashToolPatterns:
4
+ # ---------------------------------------------------------------------------
5
+ # AWS CLI DESTRUCTIVE OPERATIONS
6
+ # ---------------------------------------------------------------------------
7
+ - id: aws.aws-s3-rm-recursive-deletes-all-objects
8
+ pattern: '\baws\s+s3\s+rm\s+.*--recursive'
9
+ reason: aws s3 rm --recursive (deletes all objects)
10
+ anchored: true
11
+
12
+ - id: aws.aws-s3-rb-force-force-removes-bucket
13
+ pattern: '\baws\s+s3\s+rb\s+.*--force'
14
+ reason: aws s3 rb --force (force removes bucket)
15
+ anchored: true
16
+
17
+ - id: aws.aws-ec2-terminate-instances
18
+ pattern: '\baws\s+ec2\s+terminate-instances\b'
19
+ reason: aws ec2 terminate-instances
20
+ anchored: true
21
+
22
+ - id: aws.aws-rds-delete-db-instance
23
+ pattern: '\baws\s+rds\s+delete-db-instance\b'
24
+ reason: aws rds delete-db-instance
25
+ anchored: true
26
+
27
+ - id: aws.aws-cloudformation-delete-stack-deletes-infrastructure
28
+ pattern: '\baws\s+cloudformation\s+delete-stack\b'
29
+ reason: aws cloudformation delete-stack (deletes infrastructure)
30
+ anchored: true
31
+
32
+ - id: aws.aws-dynamodb-delete-table
33
+ pattern: '\baws\s+dynamodb\s+delete-table\b'
34
+ reason: aws dynamodb delete-table
35
+ anchored: true
36
+
37
+ - id: aws.aws-eks-delete-cluster
38
+ pattern: '\baws\s+eks\s+delete-cluster\b'
39
+ reason: aws eks delete-cluster
40
+ anchored: true
41
+
42
+ - id: aws.aws-lambda-delete-function
43
+ pattern: '\baws\s+lambda\s+delete-function\b'
44
+ reason: aws lambda delete-function
45
+ anchored: true
46
+
47
+ - id: aws.aws-iam-delete-role
48
+ pattern: '\baws\s+iam\s+delete-role\b'
49
+ reason: aws iam delete-role
50
+ anchored: true
51
+
52
+ - id: aws.aws-iam-delete-user
53
+ pattern: '\baws\s+iam\s+delete-user\b'
54
+ reason: aws iam delete-user
55
+ anchored: true
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # AWS DEPLOYS (ask: true — ship code/infra, caught in unattended sessions)
59
+ # ---------------------------------------------------------------------------
60
+ - pattern: '\baws\s+cloudformation\s+deploy\b'
61
+ reason: aws cloudformation deploy (ships an infrastructure change)
62
+ ask: true
63
+ id: deploy.aws-cloudformation
64
+ anchored: true
65
+
66
+ - pattern: '\baws\s+lambda\s+update-function-code\b'
67
+ reason: aws lambda update-function-code (ships new function code)
68
+ ask: true
69
+ id: deploy.aws-lambda-update
70
+ anchored: true
71
+
72
+ - pattern: '\baws\s+ecs\s+update-service\b'
73
+ reason: aws ecs update-service (ships an ECS deployment)
74
+ ask: true
75
+ id: deploy.aws-ecs-update
76
+ anchored: true
@@ -0,0 +1,162 @@
1
+ {
2
+ "bashToolPatterns": [
3
+ {
4
+ "id": "cloud-hosting.vercel-remove-yes-removes-deployment",
5
+ "pattern": "\\bvercel\\s+remove\\s+.*--yes",
6
+ "reason": "vercel remove --yes (removes deployment)",
7
+ "anchored": true
8
+ },
9
+ {
10
+ "id": "cloud-hosting.vercel-projects-rm-deletes-project",
11
+ "pattern": "\\bvercel\\s+projects\\s+rm\\b",
12
+ "reason": "vercel projects rm (deletes project)",
13
+ "anchored": true
14
+ },
15
+ {
16
+ "id": "cloud-hosting.vercel-env-rm-yes-removes-env-variables",
17
+ "pattern": "\\bvercel\\s+env\\s+rm\\s+.*--yes",
18
+ "reason": "vercel env rm --yes (removes env variables)",
19
+ "anchored": true
20
+ },
21
+ {
22
+ "id": "cloud-hosting.netlify-sites-delete-deletes-entire-site",
23
+ "pattern": "\\bnetlify\\s+sites:delete\\b",
24
+ "reason": "netlify sites:delete (deletes entire site)",
25
+ "anchored": true
26
+ },
27
+ {
28
+ "id": "cloud-hosting.netlify-functions-delete",
29
+ "pattern": "\\bnetlify\\s+functions:delete\\b",
30
+ "reason": "netlify functions:delete",
31
+ "anchored": true
32
+ },
33
+ {
34
+ "id": "cloud-hosting.wrangler-delete-deletes-worker",
35
+ "pattern": "\\bwrangler\\s+delete\\b",
36
+ "reason": "wrangler delete (deletes Worker)",
37
+ "anchored": true
38
+ },
39
+ {
40
+ "id": "cloud-hosting.wrangler-r2-bucket-delete",
41
+ "pattern": "\\bwrangler\\s+r2\\s+bucket\\s+delete\\b",
42
+ "reason": "wrangler r2 bucket delete",
43
+ "anchored": true
44
+ },
45
+ {
46
+ "id": "cloud-hosting.wrangler-kv-namespace-delete",
47
+ "pattern": "\\bwrangler\\s+kv:namespace\\s+delete\\b",
48
+ "reason": "wrangler kv:namespace delete",
49
+ "anchored": true
50
+ },
51
+ {
52
+ "id": "cloud-hosting.wrangler-d1-delete-deletes-database",
53
+ "pattern": "\\bwrangler\\s+d1\\s+delete\\b",
54
+ "reason": "wrangler d1 delete (deletes database)",
55
+ "anchored": true
56
+ },
57
+ {
58
+ "id": "cloud-hosting.wrangler-queues-delete",
59
+ "pattern": "\\bwrangler\\s+queues\\s+delete\\b",
60
+ "reason": "wrangler queues delete",
61
+ "anchored": true
62
+ },
63
+ {
64
+ "id": "cloud-hosting.heroku-apps-destroy",
65
+ "pattern": "\\bheroku\\s+apps:destroy\\b",
66
+ "reason": "heroku apps:destroy",
67
+ "anchored": true
68
+ },
69
+ {
70
+ "id": "cloud-hosting.heroku-pg-reset-resets-database",
71
+ "pattern": "\\bheroku\\s+pg:reset\\b",
72
+ "reason": "heroku pg:reset (resets database)",
73
+ "anchored": true
74
+ },
75
+ {
76
+ "id": "cloud-hosting.fly-apps-destroy-fly-io",
77
+ "pattern": "\\bfly\\s+apps\\s+destroy\\b",
78
+ "reason": "fly apps destroy (Fly.io)",
79
+ "anchored": true
80
+ },
81
+ {
82
+ "id": "cloud-hosting.fly-destroy-fly-io",
83
+ "pattern": "\\bfly\\s+destroy\\b",
84
+ "reason": "fly destroy (Fly.io)",
85
+ "anchored": true
86
+ },
87
+ {
88
+ "id": "cloud-hosting.doctl-droplet-delete-digitalocean",
89
+ "pattern": "\\bdoctl\\s+compute\\s+droplet\\s+delete\\b",
90
+ "reason": "doctl droplet delete (DigitalOcean)",
91
+ "anchored": true
92
+ },
93
+ {
94
+ "id": "cloud-hosting.doctl-databases-delete-digitalocean",
95
+ "pattern": "\\bdoctl\\s+databases\\s+delete\\b",
96
+ "reason": "doctl databases delete (DigitalOcean)",
97
+ "anchored": true
98
+ },
99
+ {
100
+ "id": "cloud-hosting.supabase-db-reset",
101
+ "pattern": "\\bsupabase\\s+db\\s+reset\\b",
102
+ "reason": "supabase db reset",
103
+ "anchored": true
104
+ },
105
+ {
106
+ "pattern": "\\bvercel\\s+deploy\\b",
107
+ "reason": "vercel deploy (ships a deployment)",
108
+ "ask": true,
109
+ "id": "deploy.vercel",
110
+ "anchored": true
111
+ },
112
+ {
113
+ "pattern": "\\bvercel\\s+(-[^\\s]*\\s+)*--prod\\b",
114
+ "reason": "vercel --prod (ships a production deployment)",
115
+ "ask": true,
116
+ "id": "deploy.vercel-prod",
117
+ "anchored": true
118
+ },
119
+ {
120
+ "pattern": "\\bnetlify\\s+deploy\\b",
121
+ "reason": "netlify deploy (ships a deployment)",
122
+ "ask": true,
123
+ "id": "deploy.netlify",
124
+ "anchored": true
125
+ },
126
+ {
127
+ "pattern": "\\b(fly|flyctl)\\s+deploy\\b",
128
+ "reason": "fly deploy (ships a Fly.io deployment)",
129
+ "ask": true,
130
+ "id": "deploy.fly",
131
+ "anchored": true
132
+ },
133
+ {
134
+ "pattern": "\\bwrangler\\s+(deploy|publish)\\b",
135
+ "reason": "wrangler deploy (ships a Cloudflare Worker)",
136
+ "ask": true,
137
+ "id": "deploy.wrangler",
138
+ "anchored": true
139
+ },
140
+ {
141
+ "pattern": "\\brailway\\s+(up|deploy)\\b",
142
+ "reason": "railway up (ships a Railway deployment)",
143
+ "ask": true,
144
+ "id": "deploy.railway",
145
+ "anchored": true
146
+ },
147
+ {
148
+ "pattern": "\\brender\\s+deploys?\\s+create\\b",
149
+ "reason": "render deploys create (ships a Render deployment)",
150
+ "ask": true,
151
+ "id": "deploy.render",
152
+ "anchored": true
153
+ },
154
+ {
155
+ "pattern": "\\bsupabase\\s+functions\\s+deploy\\b",
156
+ "reason": "supabase functions deploy (ships an edge function)",
157
+ "ask": true,
158
+ "id": "deploy.supabase-functions",
159
+ "anchored": true
160
+ }
161
+ ]
162
+ }
@@ -0,0 +1,163 @@
1
+ # Cloud hosting damage control rules
2
+ # Vercel, Netlify, Cloudflare, Heroku, Fly.io, DigitalOcean, Supabase, GitHub CLI, npm
3
+
4
+ bashToolPatterns:
5
+ # ---------------------------------------------------------------------------
6
+ # VERCEL DESTRUCTIVE OPERATIONS
7
+ # ---------------------------------------------------------------------------
8
+ - id: cloud-hosting.vercel-remove-yes-removes-deployment
9
+ pattern: '\bvercel\s+remove\s+.*--yes'
10
+ reason: vercel remove --yes (removes deployment)
11
+ anchored: true
12
+
13
+ - id: cloud-hosting.vercel-projects-rm-deletes-project
14
+ pattern: '\bvercel\s+projects\s+rm\b'
15
+ reason: vercel projects rm (deletes project)
16
+ anchored: true
17
+
18
+ - id: cloud-hosting.vercel-env-rm-yes-removes-env-variables
19
+ pattern: '\bvercel\s+env\s+rm\s+.*--yes'
20
+ reason: vercel env rm --yes (removes env variables)
21
+ anchored: true
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # NETLIFY DESTRUCTIVE OPERATIONS
25
+ # ---------------------------------------------------------------------------
26
+ - id: cloud-hosting.netlify-sites-delete-deletes-entire-site
27
+ pattern: '\bnetlify\s+sites:delete\b'
28
+ reason: netlify sites:delete (deletes entire site)
29
+ anchored: true
30
+
31
+ - id: cloud-hosting.netlify-functions-delete
32
+ pattern: '\bnetlify\s+functions:delete\b'
33
+ reason: netlify functions:delete
34
+ anchored: true
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # CLOUDFLARE (wrangler) DESTRUCTIVE OPERATIONS
38
+ # ---------------------------------------------------------------------------
39
+ - id: cloud-hosting.wrangler-delete-deletes-worker
40
+ pattern: '\bwrangler\s+delete\b'
41
+ reason: wrangler delete (deletes Worker)
42
+ anchored: true
43
+
44
+ - id: cloud-hosting.wrangler-r2-bucket-delete
45
+ pattern: '\bwrangler\s+r2\s+bucket\s+delete\b'
46
+ reason: wrangler r2 bucket delete
47
+ anchored: true
48
+
49
+ - id: cloud-hosting.wrangler-kv-namespace-delete
50
+ pattern: '\bwrangler\s+kv:namespace\s+delete\b'
51
+ reason: wrangler kv:namespace delete
52
+ anchored: true
53
+
54
+ - id: cloud-hosting.wrangler-d1-delete-deletes-database
55
+ pattern: '\bwrangler\s+d1\s+delete\b'
56
+ reason: wrangler d1 delete (deletes database)
57
+ anchored: true
58
+
59
+ - id: cloud-hosting.wrangler-queues-delete
60
+ pattern: '\bwrangler\s+queues\s+delete\b'
61
+ reason: wrangler queues delete
62
+ anchored: true
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # HEROKU DESTRUCTIVE OPERATIONS
66
+ # ---------------------------------------------------------------------------
67
+ - id: cloud-hosting.heroku-apps-destroy
68
+ pattern: '\bheroku\s+apps:destroy\b'
69
+ reason: heroku apps:destroy
70
+ anchored: true
71
+
72
+ - id: cloud-hosting.heroku-pg-reset-resets-database
73
+ pattern: '\bheroku\s+pg:reset\b'
74
+ reason: heroku pg:reset (resets database)
75
+ anchored: true
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # FLY.IO DESTRUCTIVE OPERATIONS
79
+ # ---------------------------------------------------------------------------
80
+ - id: cloud-hosting.fly-apps-destroy-fly-io
81
+ pattern: '\bfly\s+apps\s+destroy\b'
82
+ reason: fly apps destroy (Fly.io)
83
+ anchored: true
84
+
85
+ - id: cloud-hosting.fly-destroy-fly-io
86
+ pattern: '\bfly\s+destroy\b'
87
+ reason: fly destroy (Fly.io)
88
+ anchored: true
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # DIGITALOCEAN (doctl) DESTRUCTIVE OPERATIONS
92
+ # ---------------------------------------------------------------------------
93
+ - id: cloud-hosting.doctl-droplet-delete-digitalocean
94
+ pattern: '\bdoctl\s+compute\s+droplet\s+delete\b'
95
+ reason: doctl droplet delete (DigitalOcean)
96
+ anchored: true
97
+
98
+ - id: cloud-hosting.doctl-databases-delete-digitalocean
99
+ pattern: '\bdoctl\s+databases\s+delete\b'
100
+ reason: doctl databases delete (DigitalOcean)
101
+ anchored: true
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # SUPABASE DESTRUCTIVE OPERATIONS
105
+ # ---------------------------------------------------------------------------
106
+ - id: cloud-hosting.supabase-db-reset
107
+ pattern: '\bsupabase\s+db\s+reset\b'
108
+ reason: supabase db reset
109
+ anchored: true
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # DEPLOYS (ask: true — outward-facing, caught in unattended sessions)
113
+ # ---------------------------------------------------------------------------
114
+ # A deploy ships code to production. The matching `remove`/`destroy` verbs are
115
+ # hard-blocked above; the deploy verbs themselves were previously `allow`-tier,
116
+ # so a scheduled agent could ship to prod silently. Classify them `ask`.
117
+ - pattern: '\bvercel\s+deploy\b'
118
+ reason: vercel deploy (ships a deployment)
119
+ ask: true
120
+ id: deploy.vercel
121
+ anchored: true
122
+
123
+ - pattern: '\bvercel\s+(-[^\s]*\s+)*--prod\b'
124
+ reason: vercel --prod (ships a production deployment)
125
+ ask: true
126
+ id: deploy.vercel-prod
127
+ anchored: true
128
+
129
+ - pattern: '\bnetlify\s+deploy\b'
130
+ reason: netlify deploy (ships a deployment)
131
+ ask: true
132
+ id: deploy.netlify
133
+ anchored: true
134
+
135
+ - pattern: '\b(fly|flyctl)\s+deploy\b'
136
+ reason: fly deploy (ships a Fly.io deployment)
137
+ ask: true
138
+ id: deploy.fly
139
+ anchored: true
140
+
141
+ - pattern: '\bwrangler\s+(deploy|publish)\b'
142
+ reason: wrangler deploy (ships a Cloudflare Worker)
143
+ ask: true
144
+ id: deploy.wrangler
145
+ anchored: true
146
+
147
+ - pattern: '\brailway\s+(up|deploy)\b'
148
+ reason: railway up (ships a Railway deployment)
149
+ ask: true
150
+ id: deploy.railway
151
+ anchored: true
152
+
153
+ - pattern: '\brender\s+deploys?\s+create\b'
154
+ reason: render deploys create (ships a Render deployment)
155
+ ask: true
156
+ id: deploy.render
157
+ anchored: true
158
+
159
+ - pattern: '\bsupabase\s+functions\s+deploy\b'
160
+ reason: supabase functions deploy (ships an edge function)
161
+ ask: true
162
+ id: deploy.supabase-functions
163
+ anchored: true