@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.
- package/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +13 -0
- package/.skills.json +19 -0
- package/AGENTS.md +1344 -0
- package/CLAUDE.md +178 -0
- package/GEMINI.md +102 -0
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/SKILL.md +132 -0
- package/bin/agentic-hooks.sh +79 -0
- package/bin/cli.js +1060 -0
- package/core/__init__.py +52 -0
- package/core/ai_evaluator.py +117 -0
- package/core/autopilot_engine.py +368 -0
- package/core/clean_code_guard.py +188 -0
- package/core/engine_py/__init__.py +29 -0
- package/core/engine_py/agent_worker.py +136 -0
- package/core/engine_py/decider.py +150 -0
- package/core/engine_py/energy.py +45 -0
- package/core/engine_py/event_bus.py +63 -0
- package/core/engine_py/executor.py +186 -0
- package/core/engine_py/models.py +193 -0
- package/core/engine_py/queue.py +314 -0
- package/core/engine_py/runner.py +116 -0
- package/core/engine_py/system_workers.py +70 -0
- package/core/engine_py/toon_adapter.py +586 -0
- package/core/engine_py/verification_controller.py +208 -0
- package/core/engine_py/worker.py +167 -0
- package/core/engine_spec/event_schema.json +65 -0
- package/core/engine_spec/example_workflow.yaml +73 -0
- package/core/engine_spec/workflow_schema.json +127 -0
- package/core/hooks/__init__.py +29 -0
- package/core/hooks/adapters/__init__.py +25 -0
- package/core/hooks/adapters/claude_adapter.py +83 -0
- package/core/hooks/adapters/cli_agent_adapter.py +82 -0
- package/core/hooks/adapters/codex_adapter.py +78 -0
- package/core/hooks/adapters/cursor_adapter.py +73 -0
- package/core/hooks/adapters/gemini_adapter.py +93 -0
- package/core/hooks/adapters/homebrew_adapter.py +69 -0
- package/core/hooks/adapters/mcp_proxy.py +133 -0
- package/core/hooks/adapters/shell_adapter.py +65 -0
- package/core/hooks/dispatcher.py +118 -0
- package/core/hooks/policy_engine.py +375 -0
- package/core/hooks/session_end.py +141 -0
- package/core/hooks/types.py +147 -0
- package/core/integrations/__init__.py +28 -0
- package/core/integrations/installer.py +225 -0
- package/core/integrations/lifecycle_director.py +175 -0
- package/core/integrations/registry.py +105 -0
- package/core/multi_agent_system.py +164 -0
- package/core/skills_indexer.py +742 -0
- package/core/system/__init__.py +25 -0
- package/core/system/announcements.py +72 -0
- package/core/system/dependencies.py +69 -0
- package/core/system/doctor.py +171 -0
- package/core/system/health.py +144 -0
- package/core/system/installer.py +137 -0
- package/core/system/notifications.py +97 -0
- package/core/system/refresher.py +110 -0
- package/core/system/updater.py +167 -0
- package/core/system/version_tracker.py +65 -0
- package/docs/architecture_plan.md +7 -0
- package/docs/guides/failure-recovery.md +714 -0
- package/docs/implementation_summary.md +10 -0
- package/docs/protocols/autopilot-execution.md +148 -0
- package/docs/protocols/code-change-protocol.md +49 -0
- package/docs/protocols/context-preservation-detail.md +114 -0
- package/docs/protocols/quality-gates.md +110 -0
- package/docs/protocols/ulw-mode.md +60 -0
- package/docs/research_findings.md +10 -0
- package/docs/solutions/autonomous-autopilot-engine-architecture.md +38 -0
- package/install.sh +111 -0
- package/marketplace.json +37 -0
- package/package.json +81 -0
- package/skills/agentic-workflow/SKILL.md +132 -0
- package/skills/agentic-workflow/skill-spec.json +100 -0
- package/soul.md +445 -0
- package/src/engine_ts/decider.ts +186 -0
- package/src/engine_ts/event-bus.ts +57 -0
- package/src/engine_ts/executor.ts +262 -0
- package/src/engine_ts/index.ts +12 -0
- package/src/engine_ts/queue.ts +93 -0
- package/src/engine_ts/runner.ts +108 -0
- package/src/engine_ts/skills-indexer.ts +264 -0
- package/src/engine_ts/toon-adapter.ts +91 -0
- package/src/engine_ts/types.ts +134 -0
- package/src/engine_ts/verification-controller.ts +204 -0
- package/src/engine_ts/worker.ts +280 -0
- package/src/hooks/adapters/claude-adapter.ts +54 -0
- package/src/hooks/adapters/cli-agent-adapter.ts +46 -0
- package/src/hooks/adapters/codex-adapter.ts +69 -0
- package/src/hooks/adapters/cursor-adapter.ts +60 -0
- package/src/hooks/adapters/gemini-adapter.ts +71 -0
- package/src/hooks/adapters/homebrew-adapter.ts +36 -0
- package/src/hooks/adapters/mcp-proxy.ts +66 -0
- package/src/hooks/adapters/shell-adapter.ts +42 -0
- package/src/hooks/dispatcher.ts +113 -0
- package/src/hooks/index.ts +16 -0
- package/src/hooks/policy-engine.ts +376 -0
- package/src/hooks/session-end.ts +125 -0
- package/src/hooks/types.ts +61 -0
- package/src/index.d.ts +34 -0
- package/src/index.ts +23 -0
- package/src/integrations/index.ts +7 -0
- package/src/integrations/installer.ts +208 -0
- package/src/integrations/lifecycle-director.ts +139 -0
- package/src/integrations/registry.ts +82 -0
- package/src/system/announcements.ts +143 -0
- package/src/system/dependencies.ts +176 -0
- package/src/system/doctor.ts +374 -0
- package/src/system/health.ts +270 -0
- package/src/system/index.ts +14 -0
- package/src/system/installer.ts +262 -0
- package/src/system/notifications.ts +180 -0
- package/src/system/refresher.ts +207 -0
- package/src/system/types.ts +268 -0
- package/src/system/updater.ts +219 -0
- 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
|
+
)
|