@mamdouh-aboammar/agentic-workflow 1.2.0

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 (118) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/.codex-plugin/plugin.json +13 -0
  3. package/.skills.json +19 -0
  4. package/AGENTS.md +1344 -0
  5. package/CLAUDE.md +178 -0
  6. package/GEMINI.md +102 -0
  7. package/LICENSE +21 -0
  8. package/README.md +350 -0
  9. package/SKILL.md +132 -0
  10. package/bin/agentic-hooks.sh +79 -0
  11. package/bin/cli.js +1060 -0
  12. package/core/__init__.py +52 -0
  13. package/core/ai_evaluator.py +117 -0
  14. package/core/autopilot_engine.py +368 -0
  15. package/core/clean_code_guard.py +188 -0
  16. package/core/engine_py/__init__.py +29 -0
  17. package/core/engine_py/agent_worker.py +136 -0
  18. package/core/engine_py/decider.py +150 -0
  19. package/core/engine_py/energy.py +45 -0
  20. package/core/engine_py/event_bus.py +63 -0
  21. package/core/engine_py/executor.py +186 -0
  22. package/core/engine_py/models.py +193 -0
  23. package/core/engine_py/queue.py +314 -0
  24. package/core/engine_py/runner.py +116 -0
  25. package/core/engine_py/system_workers.py +70 -0
  26. package/core/engine_py/toon_adapter.py +586 -0
  27. package/core/engine_py/verification_controller.py +208 -0
  28. package/core/engine_py/worker.py +167 -0
  29. package/core/engine_spec/event_schema.json +65 -0
  30. package/core/engine_spec/example_workflow.yaml +73 -0
  31. package/core/engine_spec/workflow_schema.json +127 -0
  32. package/core/hooks/__init__.py +29 -0
  33. package/core/hooks/adapters/__init__.py +25 -0
  34. package/core/hooks/adapters/claude_adapter.py +83 -0
  35. package/core/hooks/adapters/cli_agent_adapter.py +82 -0
  36. package/core/hooks/adapters/codex_adapter.py +78 -0
  37. package/core/hooks/adapters/cursor_adapter.py +73 -0
  38. package/core/hooks/adapters/gemini_adapter.py +93 -0
  39. package/core/hooks/adapters/homebrew_adapter.py +69 -0
  40. package/core/hooks/adapters/mcp_proxy.py +133 -0
  41. package/core/hooks/adapters/shell_adapter.py +65 -0
  42. package/core/hooks/dispatcher.py +118 -0
  43. package/core/hooks/policy_engine.py +375 -0
  44. package/core/hooks/session_end.py +141 -0
  45. package/core/hooks/types.py +147 -0
  46. package/core/integrations/__init__.py +28 -0
  47. package/core/integrations/installer.py +225 -0
  48. package/core/integrations/lifecycle_director.py +175 -0
  49. package/core/integrations/registry.py +105 -0
  50. package/core/multi_agent_system.py +164 -0
  51. package/core/skills_indexer.py +742 -0
  52. package/core/system/__init__.py +25 -0
  53. package/core/system/announcements.py +72 -0
  54. package/core/system/dependencies.py +69 -0
  55. package/core/system/doctor.py +171 -0
  56. package/core/system/health.py +144 -0
  57. package/core/system/installer.py +137 -0
  58. package/core/system/notifications.py +97 -0
  59. package/core/system/refresher.py +110 -0
  60. package/core/system/updater.py +167 -0
  61. package/core/system/version_tracker.py +65 -0
  62. package/docs/architecture_plan.md +7 -0
  63. package/docs/guides/failure-recovery.md +714 -0
  64. package/docs/implementation_summary.md +10 -0
  65. package/docs/protocols/autopilot-execution.md +148 -0
  66. package/docs/protocols/code-change-protocol.md +49 -0
  67. package/docs/protocols/context-preservation-detail.md +114 -0
  68. package/docs/protocols/quality-gates.md +110 -0
  69. package/docs/protocols/ulw-mode.md +60 -0
  70. package/docs/research_findings.md +10 -0
  71. package/docs/solutions/autonomous-autopilot-engine-architecture.md +38 -0
  72. package/install.sh +111 -0
  73. package/marketplace.json +37 -0
  74. package/package.json +81 -0
  75. package/skills/agentic-workflow/SKILL.md +132 -0
  76. package/skills/agentic-workflow/skill-spec.json +100 -0
  77. package/soul.md +445 -0
  78. package/src/engine_ts/decider.ts +186 -0
  79. package/src/engine_ts/event-bus.ts +57 -0
  80. package/src/engine_ts/executor.ts +262 -0
  81. package/src/engine_ts/index.ts +12 -0
  82. package/src/engine_ts/queue.ts +93 -0
  83. package/src/engine_ts/runner.ts +108 -0
  84. package/src/engine_ts/skills-indexer.ts +264 -0
  85. package/src/engine_ts/toon-adapter.ts +91 -0
  86. package/src/engine_ts/types.ts +134 -0
  87. package/src/engine_ts/verification-controller.ts +204 -0
  88. package/src/engine_ts/worker.ts +280 -0
  89. package/src/hooks/adapters/claude-adapter.ts +54 -0
  90. package/src/hooks/adapters/cli-agent-adapter.ts +46 -0
  91. package/src/hooks/adapters/codex-adapter.ts +69 -0
  92. package/src/hooks/adapters/cursor-adapter.ts +60 -0
  93. package/src/hooks/adapters/gemini-adapter.ts +71 -0
  94. package/src/hooks/adapters/homebrew-adapter.ts +36 -0
  95. package/src/hooks/adapters/mcp-proxy.ts +66 -0
  96. package/src/hooks/adapters/shell-adapter.ts +42 -0
  97. package/src/hooks/dispatcher.ts +113 -0
  98. package/src/hooks/index.ts +16 -0
  99. package/src/hooks/policy-engine.ts +376 -0
  100. package/src/hooks/session-end.ts +125 -0
  101. package/src/hooks/types.ts +61 -0
  102. package/src/index.d.ts +34 -0
  103. package/src/index.ts +23 -0
  104. package/src/integrations/index.ts +7 -0
  105. package/src/integrations/installer.ts +208 -0
  106. package/src/integrations/lifecycle-director.ts +139 -0
  107. package/src/integrations/registry.ts +82 -0
  108. package/src/system/announcements.ts +143 -0
  109. package/src/system/dependencies.ts +176 -0
  110. package/src/system/doctor.ts +374 -0
  111. package/src/system/health.ts +270 -0
  112. package/src/system/index.ts +14 -0
  113. package/src/system/installer.ts +262 -0
  114. package/src/system/notifications.ts +180 -0
  115. package/src/system/refresher.ts +207 -0
  116. package/src/system/types.ts +268 -0
  117. package/src/system/updater.ts +219 -0
  118. package/src/system/version-tracker.ts +137 -0
@@ -0,0 +1,65 @@
1
+ """
2
+ Shell & Terminal Hook Adapter
3
+ =============================
4
+ Intercepts commands in Bash, Zsh, and interactive/scripted terminal sessions.
5
+ Integrates with shell hooks (preexec/precmd, DEBUG trap) and shell wrapper shims.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ from typing import List, Optional
11
+
12
+ from ..types import (
13
+ HookEvent,
14
+ HookResult,
15
+ HookSource,
16
+ HookType,
17
+ HookVerdict,
18
+ )
19
+ from ..policy_engine import UniversalPolicyEngine
20
+
21
+
22
+ class ShellHookAdapter:
23
+ """Intercepts and governs raw shell / terminal command execution."""
24
+
25
+ def __init__(self, policy_engine: Optional[UniversalPolicyEngine] = None):
26
+ self.engine = policy_engine or UniversalPolicyEngine()
27
+
28
+ def create_event(
29
+ self,
30
+ command: str,
31
+ hook_type: HookType = HookType.PRE_COMMAND,
32
+ source: HookSource = HookSource.BASH,
33
+ cwd: Optional[str] = None,
34
+ ) -> HookEvent:
35
+ return HookEvent(
36
+ source=source,
37
+ hook_type=hook_type,
38
+ command=command.strip(),
39
+ tool_name="bash",
40
+ cwd=cwd or os.getcwd(),
41
+ env=dict(os.environ),
42
+ )
43
+
44
+ def evaluate_command(
45
+ self,
46
+ command: str,
47
+ hook_type: HookType = HookType.PRE_COMMAND,
48
+ source: HookSource = HookSource.BASH,
49
+ ) -> HookResult:
50
+ if not command or not command.strip():
51
+ return HookResult(event_id="empty_cmd", verdict=HookVerdict.ALLOW, exit_code=0)
52
+
53
+ event = self.create_event(command, hook_type, source)
54
+ return self.engine.evaluate(event)
55
+
56
+ def run_cli_preexec(self, command: str) -> int:
57
+ result = self.evaluate_command(command, hook_type=HookType.PRE_COMMAND, source=HookSource.TERMINAL)
58
+ if result.verdict == HookVerdict.BLOCK:
59
+ print(f"\033[1;31m🛑 [Agentic Shell Hook] EXECUTION BLOCKED:\033[0m\n {result.message}", file=sys.stderr)
60
+ print(f" Command: {command[:200]}\033[0m", file=sys.stderr)
61
+ return 2
62
+ elif result.verdict == HookVerdict.WARN:
63
+ print(f"\033[1;33m⚠️ [Agentic Shell Hook] ADVISORY:\033[0m {result.message}", file=sys.stderr)
64
+ return 0
65
+ return 0
@@ -0,0 +1,118 @@
1
+ """
2
+ Universal Agentic Hooks Framework (UAHF) — Dispatcher
3
+ =====================================================
4
+ Central orchestrator for receiving, routing, evaluating, and logging hook events.
5
+ Maintains event ledger append-only audit trail and workflow state synchronization.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ from pathlib import Path
12
+ import time
13
+ from typing import Any, Dict, Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ from .types import (
18
+ HookEvent,
19
+ HookResult,
20
+ HookSource,
21
+ HookType,
22
+ HookVerdict,
23
+ )
24
+ from .policy_engine import UniversalPolicyEngine
25
+ from .adapters.claude_adapter import ClaudeHookAdapter
26
+ from .adapters.gemini_adapter import GeminiHookAdapter
27
+ from .adapters.cursor_adapter import CursorHookAdapter
28
+ from .adapters.codex_adapter import CodexHookAdapter
29
+ from .adapters.shell_adapter import ShellHookAdapter
30
+ from .adapters.homebrew_adapter import HomebrewHookAdapter
31
+ from .adapters.mcp_proxy import McpHookProxy
32
+ from .adapters.cli_agent_adapter import CliAgentAdapter
33
+
34
+
35
+ class HookDispatcher:
36
+ """Dispatches hook invocations to respective platform adapters and logs verdicts."""
37
+
38
+ def __init__(self, project_dir: Optional[str] = None):
39
+ self.project_dir = Path(project_dir) if project_dir else Path.cwd()
40
+ self.policy_engine = UniversalPolicyEngine(str(self.project_dir))
41
+
42
+ # Register platform adapters
43
+ self.claude_adapter = ClaudeHookAdapter(self.policy_engine)
44
+ self.gemini_adapter = GeminiHookAdapter(self.policy_engine)
45
+ self.cursor_adapter = CursorHookAdapter(self.policy_engine)
46
+ self.codex_adapter = CodexHookAdapter(self.policy_engine)
47
+ self.shell_adapter = ShellHookAdapter(self.policy_engine)
48
+ self.homebrew_adapter = HomebrewHookAdapter(self.policy_engine)
49
+ self.mcp_proxy = McpHookProxy(self.policy_engine)
50
+ self.cli_adapter = CliAgentAdapter(self.policy_engine)
51
+
52
+ self.ledger_file = self.project_dir / ".traces" / "hook_events.jsonl"
53
+ self._ensure_ledger()
54
+
55
+ def _ensure_ledger(self):
56
+ self.ledger_file.parent.mkdir(parents=True, exist_ok=True)
57
+ if not self.ledger_file.exists():
58
+ self.ledger_file.touch()
59
+
60
+ def log_event_ledger(self, event: HookEvent, result: HookResult):
61
+ """Append hook execution to audit ledger for non-repudiation."""
62
+ entry = {
63
+ "timestamp": time.time(),
64
+ "event_id": event.event_id,
65
+ "source": event.source.value if isinstance(event.source, HookSource) else str(event.source),
66
+ "hook_type": event.hook_type.value if isinstance(event.hook_type, HookType) else str(event.hook_type),
67
+ "tool": event.tool_name,
68
+ "command": (event.command[:120] + "...") if event.command and len(event.command) > 120 else event.command,
69
+ "file": event.file_path,
70
+ "verdict": result.verdict.value if isinstance(result.verdict, HookVerdict) else str(result.verdict),
71
+ "rule_id": result.rule_id,
72
+ "message": result.message,
73
+ }
74
+ try:
75
+ with open(self.ledger_file, "a", encoding="utf-8") as f:
76
+ f.write(json.dumps(entry) + "\n")
77
+ except Exception as log_err:
78
+ logger.debug("Failed to write to audit ledger: %s", log_err)
79
+
80
+ def dispatch(self, event: HookEvent) -> HookResult:
81
+ """Route event through policy pipeline, record audit trail, and return verdict."""
82
+ result = self.policy_engine.evaluate(event)
83
+ self.log_event_ledger(event, result)
84
+ return result
85
+
86
+ def get_status(self) -> Dict[str, Any]:
87
+ """Return runtime status of all hook adapters and policies."""
88
+ total_events = 0
89
+ blocked_events = 0
90
+ if self.ledger_file.exists():
91
+ with open(self.ledger_file, "r", encoding="utf-8") as f:
92
+ for line in f:
93
+ if not line.strip():
94
+ continue
95
+ total_events += 1
96
+ if '"verdict": "block"' in line:
97
+ blocked_events += 1
98
+
99
+ return {
100
+ "framework": "Universal Agentic Hooks Framework (UAHF)",
101
+ "version": "1.0.0",
102
+ "active_policies": [r.rule_id for r in self.policy_engine.rules],
103
+ "supported_adapters": [
104
+ "claude (native JSON)",
105
+ "cursor (MDC + MCP)",
106
+ "antigravity (MCP + shell)",
107
+ "codex (wrapper + shell)",
108
+ "kimi (wrapper + shell)",
109
+ "bash / zsh / terminal (preexec/trap)",
110
+ "homebrew (package gatekeeper)",
111
+ "mcp (JSON-RPC stdio proxy)",
112
+ ],
113
+ "ledger_path": str(self.ledger_file),
114
+ "telemetry": {
115
+ "total_events_intercepted": total_events,
116
+ "blocked_events": blocked_events,
117
+ },
118
+ }
@@ -0,0 +1,375 @@
1
+ """
2
+ Universal Agentic Hooks Framework (UAHF) — Policy Engine
3
+ ========================================================
4
+ Centralized, deterministic policy evaluation pipeline for governing agent behavior.
5
+ Integrates safety hooks, secret sanitizers, package policy, and TDD guards.
6
+ """
7
+
8
+ from pathlib import Path
9
+ import re
10
+ import sys
11
+ from typing import Any, Dict, List, Optional, Set
12
+
13
+ from .types import (
14
+ HookEvent,
15
+ HookResult,
16
+ HookSource,
17
+ HookType,
18
+ HookVerdict,
19
+ PolicyRule,
20
+ )
21
+
22
+
23
+ class DestructiveCommandRule(PolicyRule):
24
+ """Blocks catastrophic commands (rm -rf /, dd if=, mkfs, git -f, curl | sh)."""
25
+ rule_id = "SEC-001-DESTRUCTIVE-COMMAND"
26
+ description = "Blocks destructive system, git, and exfiltration commands"
27
+
28
+ NETWORK_PATTERNS = [
29
+ (re.compile(r"\bcurl\b.*\|\s*(ba)?sh\b"), "curl piped to shell is blocked. Download, inspect, and execute manually."),
30
+ (re.compile(r"\bwget\b.*\|\s*(ba)?sh\b"), "wget piped to shell is blocked. Download, inspect, and execute manually."),
31
+ ]
32
+
33
+ SYSTEM_PATTERNS = [
34
+ (re.compile(r"\bdd\b\s+if="), "dd command with raw input file is blocked. Irreversible disk write risk."),
35
+ (re.compile(r"\bmkfs\b"), "mkfs command is blocked. Filesystem formatting destroys all data."),
36
+ ]
37
+
38
+ GIT_PATTERNS = [
39
+ (
40
+ re.compile(r"\bgit\s+push\b.*(?<![-\w])--force(?![-\w])"),
41
+ "git push --force is blocked. Use --force-with-lease to protect remote history.",
42
+ ),
43
+ (
44
+ re.compile(r"\bgit\s+push\b.*\s-[a-zA-Z]*f"),
45
+ "git push -f is blocked. Use --force-with-lease to protect remote history.",
46
+ ),
47
+ (re.compile(r"\bgit\s+reset\b.*\s--hard\b"), "git reset --hard is blocked. Discards uncommitted work permanently."),
48
+ (re.compile(r"\bgit\s+checkout\b\s+(?:--\s+)?\."), "git checkout . is blocked. Discards unstaged modifications."),
49
+ (re.compile(r"\bgit\s+restore\b\s+\."), "git restore . is blocked. Discards unstaged modifications."),
50
+ (re.compile(r"\bgit\s+clean\b.*\s-[a-zA-Z]*f"), "git clean -f is blocked. Permanently deletes untracked files."),
51
+ (re.compile(r"\bgit\s+branch\b.*\s-D\b"), "git branch -D is blocked. Use git branch -d for safe deletion."),
52
+ (
53
+ re.compile(r"\bgit\s+branch\b.*\s--delete\b.*\s--force\b"),
54
+ "git branch --delete --force is blocked. Use git branch -d for safe deletion.",
55
+ ),
56
+ ]
57
+
58
+ DANGEROUS_TARGETS = {"/", "/*", "~", "~/", "$HOME", "$HOME/", "$HOME/*"}
59
+
60
+ def _check_dangerous_rm(self, sub_command: str) -> Optional[str]:
61
+ tokens = sub_command.split()
62
+ if not tokens or tokens[0] != "rm":
63
+ return None
64
+
65
+ flags = ""
66
+ targets = []
67
+ for token in tokens[1:]:
68
+ if token.startswith("-") and not token.startswith("--"):
69
+ flags += token[1:]
70
+ elif not token.startswith("-"):
71
+ targets.append(token.strip("\"'"))
72
+
73
+ has_recursive = "r" in flags or "R" in flags
74
+ has_force = "f" in flags
75
+
76
+ if not (has_recursive and has_force):
77
+ return None
78
+
79
+ for target in targets:
80
+ if target in self.DANGEROUS_TARGETS:
81
+ return f"rm -rf targeting {target} is blocked. Catastrophic, irreversible file deletion."
82
+ return None
83
+
84
+ def evaluate(self, event: HookEvent) -> Optional[HookResult]:
85
+ command = event.command or (event.args.get("command") if isinstance(event.args, dict) else None)
86
+ if not command or not isinstance(command, str):
87
+ return None
88
+
89
+ for pattern, msg in self.NETWORK_PATTERNS:
90
+ if pattern.search(command):
91
+ return HookResult(
92
+ event_id=event.event_id,
93
+ verdict=HookVerdict.BLOCK,
94
+ message=f"DESTRUCTIVE COMMAND BLOCKED: {msg}",
95
+ exit_code=2,
96
+ rule_id=self.rule_id,
97
+ )
98
+
99
+ for pattern, msg in self.SYSTEM_PATTERNS:
100
+ if pattern.search(command):
101
+ return HookResult(
102
+ event_id=event.event_id,
103
+ verdict=HookVerdict.BLOCK,
104
+ message=f"DESTRUCTIVE COMMAND BLOCKED: {msg}",
105
+ exit_code=2,
106
+ rule_id=self.rule_id,
107
+ )
108
+
109
+ for pattern, msg in self.GIT_PATTERNS:
110
+ if pattern.search(command):
111
+ return HookResult(
112
+ event_id=event.event_id,
113
+ verdict=HookVerdict.BLOCK,
114
+ message=f"DESTRUCTIVE COMMAND BLOCKED: {msg}",
115
+ exit_code=2,
116
+ rule_id=self.rule_id,
117
+ )
118
+
119
+ for sub_cmd in re.split(r"\s*(?:&&|\|\||;)\s*", command):
120
+ for segment in sub_cmd.split("|"):
121
+ rm_err = self._check_dangerous_rm(segment.strip())
122
+ if rm_err:
123
+ return HookResult(
124
+ event_id=event.event_id,
125
+ verdict=HookVerdict.BLOCK,
126
+ message=f"DESTRUCTIVE COMMAND BLOCKED: {rm_err}",
127
+ exit_code=2,
128
+ rule_id=self.rule_id,
129
+ )
130
+
131
+ return None
132
+
133
+
134
+ class PackagePolicyRule(PolicyRule):
135
+ """Enforces package manager hygiene: bans Colima, restricts sudo brew, verifies safe installs."""
136
+ rule_id = "PKG-002-PACKAGE-HYGIENE"
137
+ description = "Enforces container and package manager governance policies"
138
+
139
+ FORBIDDEN_PACKAGES = {
140
+ "colima": "Colima is prohibited per project constitution due to large footprint. Use lightweight alternatives.",
141
+ }
142
+
143
+ BREW_INSTALL_REGEX = re.compile(r"\bbrew\s+(?:install|reinstall|cask)\s+([^\s;]+)", re.IGNORECASE)
144
+ SUDO_BREW_REGEX = re.compile(r"\bsudo\s+brew\b", re.IGNORECASE)
145
+
146
+ def evaluate(self, event: HookEvent) -> Optional[HookResult]:
147
+ command = event.command or (event.args.get("command") if isinstance(event.args, dict) else None)
148
+ args_target = event.args.get("target") or event.args.get("package")
149
+
150
+ if command and isinstance(command, str):
151
+ if self.SUDO_BREW_REGEX.search(command):
152
+ return HookResult(
153
+ event_id=event.event_id,
154
+ verdict=HookVerdict.BLOCK,
155
+ message="PACKAGE POLICY VIOLATION: 'sudo brew' is prohibited. Homebrew must not run as root.",
156
+ exit_code=2,
157
+ rule_id=self.rule_id,
158
+ )
159
+
160
+ match = self.BREW_INSTALL_REGEX.search(command)
161
+ if match:
162
+ pkg_candidate = match.group(1).lower().strip("\"'")
163
+ for forbidden_pkg, reason in self.FORBIDDEN_PACKAGES.items():
164
+ if forbidden_pkg in pkg_candidate:
165
+ return HookResult(
166
+ event_id=event.event_id,
167
+ verdict=HookVerdict.BLOCK,
168
+ message=f"PACKAGE POLICY VIOLATION: Package '{forbidden_pkg}' is blocked. {reason}",
169
+ exit_code=2,
170
+ rule_id=self.rule_id,
171
+ )
172
+
173
+ if args_target and isinstance(args_target, str):
174
+ tgt = args_target.lower()
175
+ for forbidden_pkg, reason in self.FORBIDDEN_PACKAGES.items():
176
+ if forbidden_pkg == tgt or forbidden_pkg in tgt:
177
+ return HookResult(
178
+ event_id=event.event_id,
179
+ verdict=HookVerdict.BLOCK,
180
+ message=f"PACKAGE POLICY VIOLATION: Package '{forbidden_pkg}' is blocked. {reason}",
181
+ exit_code=2,
182
+ rule_id=self.rule_id,
183
+ )
184
+
185
+ return None
186
+
187
+
188
+ class SensitiveFileRule(PolicyRule):
189
+ """Prevents overwriting or exposing security-sensitive files (.env, keys, certs)."""
190
+ rule_id = "SEC-003-SENSITIVE-FILES"
191
+ description = "Prevents tampering with credentials, environment secrets, and private keys"
192
+
193
+ SENSITIVE_PATTERNS = [
194
+ (re.compile(r"(^|[/\\])\.env(\.[a-zA-Z0-9_-]+)?$"), "Environment secret file (.env)"),
195
+ (re.compile(r"\.(pem|key|p12|pfx)$", re.IGNORECASE), "Private key or certificate"),
196
+ (re.compile(r"(^|[/\\])id_(rsa|ed25519|ecdsa|dsa)$"), "SSH private key"),
197
+ (re.compile(r"(^|[/\\])(credentials|secrets|passwords)\.(json|ya?ml|toml)$", re.IGNORECASE), "Secrets store"),
198
+ (re.compile(r"(^|[/\\])(service[-_]?account|token)\.json$", re.IGNORECASE), "Service account / Token file"),
199
+ (re.compile(r"\.(tfstate|tfvars)$", re.IGNORECASE), "Terraform state / variable secrets"),
200
+ ]
201
+
202
+ def evaluate(self, event: HookEvent) -> Optional[HookResult]:
203
+ file_path = event.file_path or event.args.get("file_path") or event.args.get("path")
204
+ if not file_path or not isinstance(file_path, str):
205
+ return None
206
+
207
+ for pattern, label in self.SENSITIVE_PATTERNS:
208
+ if pattern.search(file_path):
209
+ # If tool attempts write/edit on sensitive file, block or warn
210
+ tool_name = (event.tool_name or "").lower()
211
+ is_write = (
212
+ "write" in tool_name
213
+ or "edit" in tool_name
214
+ or event.hook_type in [HookType.PRE_TOOL, HookType.PRE_COMMAND]
215
+ )
216
+ if is_write:
217
+ return HookResult(
218
+ event_id=event.event_id,
219
+ verdict=HookVerdict.WARN,
220
+ message=f"SECURITY ADVISORY: Access/modification to sensitive file '{file_path}' ({label}).",
221
+ exit_code=0,
222
+ rule_id=self.rule_id,
223
+ )
224
+ return None
225
+
226
+
227
+ class TddIntegrityRule(PolicyRule):
228
+ """Enforces TDD discipline by blocking test file edits when .tdd-guard is active."""
229
+ rule_id = "GOV-004-TDD-INTEGRITY"
230
+ description = "Protects test files from modification during implementation phases"
231
+
232
+ TEST_FILE_PATTERNS = [
233
+ re.compile(r"(^|[/\\])test_[^/\\]+\.py$"),
234
+ re.compile(r"[._]test\.[jt]sx?$"),
235
+ re.compile(r"[._]spec\.[jt]sx?$"),
236
+ ]
237
+
238
+ def __init__(self, project_dir: Optional[str] = None):
239
+ self.project_dir = Path(project_dir) if project_dir else Path.cwd()
240
+
241
+ def is_guard_active(self) -> bool:
242
+ return (self.project_dir / ".tdd-guard").exists() or Path(".tdd-guard").exists()
243
+
244
+ def evaluate(self, event: HookEvent) -> Optional[HookResult]:
245
+ if not self.is_guard_active():
246
+ return None
247
+
248
+ file_path = event.file_path or event.args.get("file_path") or event.args.get("path")
249
+ if not file_path or not isinstance(file_path, str):
250
+ return None
251
+
252
+ tool_name = (event.tool_name or "").lower()
253
+ if not ("edit" in tool_name or "write" in tool_name):
254
+ return None
255
+
256
+ for pattern in self.TEST_FILE_PATTERNS:
257
+ if pattern.search(file_path):
258
+ return HookResult(
259
+ event_id=event.event_id,
260
+ verdict=HookVerdict.BLOCK,
261
+ message=(
262
+ f"TDD GUARD ACTIVE: Modification of test file '{file_path}' is blocked. "
263
+ "Modify implementation code to make tests pass."
264
+ ),
265
+ exit_code=2,
266
+ rule_id=self.rule_id,
267
+ )
268
+ return None
269
+
270
+
271
+ class SecretLeakRule(PolicyRule):
272
+ """Scans tool outputs and command responses for accidental credential exfiltration."""
273
+ rule_id = "SEC-005-SECRET-LEAK-FILTER"
274
+ description = "Detects and blocks leakage of API keys and authentication tokens in telemetry"
275
+
276
+ PATTERNS = [
277
+ (re.compile(r"sk-(?:proj|ant|live)-[a-zA-Z0-9_\-]{20,}"), "OpenAI / Anthropic API Key"),
278
+ (re.compile(r"AKIA[0-9A-Z]{16}"), "AWS Access Key ID"),
279
+ (re.compile(r"gh[pousr][_-][A-Za-z0-9_]{36,255}"), "GitHub Personal Access Token"),
280
+ (re.compile(r"xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,32}"), "Slack Token"),
281
+ (re.compile(r"-----BEGIN (?:RSA )?PRIVATE KEY-----"), "Private Cryptographic Key"),
282
+ ]
283
+
284
+ def evaluate(self, event: HookEvent) -> Optional[HookResult]:
285
+ output = event.output
286
+ if output is None:
287
+ return None
288
+
289
+ text = str(output)
290
+ for pattern, label in self.PATTERNS:
291
+ if pattern.search(text):
292
+ return HookResult(
293
+ event_id=event.event_id,
294
+ verdict=HookVerdict.WARN,
295
+ message=f"SECRET LEAK DETECTED: {label} found in output stream. Redacting.",
296
+ exit_code=0,
297
+ rule_id=self.rule_id,
298
+ metadata={"leak_type": label},
299
+ )
300
+ return None
301
+
302
+
303
+ class CircuitBreakerRule(PolicyRule):
304
+ """Trips and halts runaway agent loops when consecutive failure streaks >= 2."""
305
+ rule_id = "RES-006-CIRCUIT-BREAKER"
306
+ description = "Halts speculative retry loops when failure threshold is exceeded"
307
+
308
+ def __init__(self, failure_threshold: int = 2):
309
+ self.failure_threshold = failure_threshold
310
+ self.streak_counters: Dict[str, int] = {}
311
+
312
+ def record_failure(self, agent_id: str):
313
+ self.streak_counters[agent_id] = self.streak_counters.get(agent_id, 0) + 1
314
+
315
+ def record_success(self, agent_id: str):
316
+ self.streak_counters[agent_id] = 0
317
+
318
+ def evaluate(self, event: HookEvent) -> Optional[HookResult]:
319
+ agent_id = event.agent_id or "default_agent"
320
+ streak = self.streak_counters.get(agent_id, 0)
321
+ if streak >= self.failure_threshold:
322
+ return HookResult(
323
+ event_id=event.event_id,
324
+ verdict=HookVerdict.BLOCK,
325
+ message=(
326
+ f"CIRCUIT BREAKER TRIPPED: Agent '{agent_id}' has {streak} consecutive failures. "
327
+ "Halting speculative edits. Run Abductive Diagnosis before retrying."
328
+ ),
329
+ exit_code=2,
330
+ rule_id=self.rule_id,
331
+ )
332
+ return None
333
+
334
+
335
+ class UniversalPolicyEngine:
336
+ """Orchestrates all policy rules and computes conclusive hook verdicts."""
337
+
338
+ def __init__(self, project_dir: Optional[str] = None):
339
+ self.project_dir = project_dir or str(Path.cwd())
340
+ self.rules: List[PolicyRule] = [
341
+ DestructiveCommandRule(),
342
+ PackagePolicyRule(),
343
+ SensitiveFileRule(),
344
+ TddIntegrityRule(self.project_dir),
345
+ SecretLeakRule(),
346
+ CircuitBreakerRule(),
347
+ ]
348
+
349
+ def add_rule(self, rule: PolicyRule):
350
+ self.rules.append(rule)
351
+
352
+ def evaluate(self, event: HookEvent) -> HookResult:
353
+ warnings = []
354
+ for rule in self.rules:
355
+ result = rule.evaluate(event)
356
+ if result:
357
+ if result.verdict == HookVerdict.BLOCK:
358
+ return result
359
+ elif result.verdict == HookVerdict.WARN:
360
+ warnings.append(result.message)
361
+
362
+ if warnings:
363
+ return HookResult(
364
+ event_id=event.event_id,
365
+ verdict=HookVerdict.WARN,
366
+ message="; ".join(warnings),
367
+ exit_code=0,
368
+ )
369
+
370
+ return HookResult(
371
+ event_id=event.event_id,
372
+ verdict=HookVerdict.ALLOW,
373
+ message="Operation allowed by policy",
374
+ exit_code=0,
375
+ )