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.
- damage_control/__init__.py +9 -0
- damage_control/__main__.py +7 -0
- damage_control/adapter.py +179 -0
- damage_control/audit.py +432 -0
- damage_control/breakglass.py +155 -0
- damage_control/claude_code.py +148 -0
- damage_control/cli.py +173 -0
- damage_control/engine.py +10656 -0
- damage_control/groups.py +114 -0
- damage_control/harness_config.py +205 -0
- damage_control/hook.py +54 -0
- damage_control/install.py +618 -0
- damage_control/packs.py +219 -0
- damage_control/policy.py +323 -0
- damage_control/profile.py +177 -0
- damage_control/rules/aws.json +85 -0
- damage_control/rules/aws.yaml +76 -0
- damage_control/rules/cloud-hosting.json +162 -0
- damage_control/rules/cloud-hosting.yaml +163 -0
- damage_control/rules/containers.json +67 -0
- damage_control/rules/containers.yaml +69 -0
- damage_control/rules/control-plane.json +7 -0
- damage_control/rules/control-plane.yaml +21 -0
- damage_control/rules/core.json +332 -0
- damage_control/rules/core.yaml +441 -0
- damage_control/rules/databases.json +117 -0
- damage_control/rules/databases.yaml +123 -0
- damage_control/rules/firebase.json +34 -0
- damage_control/rules/firebase.yaml +30 -0
- damage_control/rules/gcp.json +60 -0
- damage_control/rules/gcp.yaml +57 -0
- damage_control/rules/git.json +119 -0
- damage_control/rules/git.yaml +120 -0
- damage_control/rules/infrastructure.json +69 -0
- damage_control/rules/infrastructure.yaml +66 -0
- damage_control/rules/mcp-tools.json +11 -0
- damage_control/rules/mcp-tools.yaml +47 -0
- damage_control/rules/outbound.json +40 -0
- damage_control/rules/outbound.yaml +58 -0
- damage_control/rules/payloads.json +120 -0
- damage_control/rules/payloads.yaml +155 -0
- damage_control/rules/publish.json +46 -0
- damage_control/rules/publish.yaml +46 -0
- damage_control/rules/registry.json +10 -0
- damage_control/rules/registry.yaml +11 -0
- damage_control/rules/remote.json +31 -0
- damage_control/rules/remote.yaml +53 -0
- damage_control/rules/tmux.json +17 -0
- damage_control/rules/tmux.yaml +16 -0
- damage_control/ruleset.py +94 -0
- damage_control/tooldefs/aws.json +140 -0
- damage_control/tooldefs/aws.yaml +119 -0
- damage_control/tooldefs/docker.json +146 -0
- damage_control/tooldefs/docker.yaml +123 -0
- damage_control/tooldefs/gcp.json +186 -0
- damage_control/tooldefs/gcp.yaml +158 -0
- damage_control/tooldefs/gh.json +172 -0
- damage_control/tooldefs/gh.yaml +145 -0
- damage_control/tooldefs/git.json +166 -0
- damage_control/tooldefs/git.yaml +143 -0
- damage_control/tooldefs/kubectl.json +101 -0
- damage_control/tooldefs/kubectl.yaml +85 -0
- damage_control/tooldefs/npm.json +77 -0
- damage_control/tooldefs/npm.yaml +65 -0
- damage_control/tooldefs/terraform.json +68 -0
- damage_control/tooldefs/terraform.yaml +57 -0
- damage_control/tooldefs/uv.json +112 -0
- damage_control/tooldefs/uv.yaml +97 -0
- damage_control_dev-0.1.0.dist-info/METADATA +15 -0
- damage_control_dev-0.1.0.dist-info/RECORD +74 -0
- damage_control_dev-0.1.0.dist-info/WHEEL +4 -0
- damage_control_dev-0.1.0.dist-info/entry_points.txt +2 -0
- damage_control_dev-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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
|