devcouncil 0.2.0 → 0.2.1
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/README.md +191 -253
- package/bin/devcouncil.js +130 -32
- package/package.json +6 -17
- package/pyproject.toml +0 -66
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +0 -4
- package/src/devcouncil/app/__init__.py +0 -28
- package/src/devcouncil/app/config.py +0 -320
- package/src/devcouncil/app/errors.py +0 -23
- package/src/devcouncil/app/events.py +0 -44
- package/src/devcouncil/app/orchestrator.py +0 -92
- package/src/devcouncil/app/project_status.py +0 -29
- package/src/devcouncil/app/run_context.py +0 -39
- package/src/devcouncil/app/state_machine.py +0 -108
- package/src/devcouncil/artifacts/__init__.py +0 -1
- package/src/devcouncil/artifacts/coverage.py +0 -96
- package/src/devcouncil/artifacts/graph.py +0 -163
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/artifacts/validators.py +0 -27
- package/src/devcouncil/assets/__init__.py +0 -1
- package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/agents.py +0 -292
- package/src/devcouncil/cli/commands/artifacts.py +0 -54
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/check.py +0 -209
- package/src/devcouncil/cli/commands/config.py +0 -115
- package/src/devcouncil/cli/commands/cost.py +0 -57
- package/src/devcouncil/cli/commands/dashboard.py +0 -31
- package/src/devcouncil/cli/commands/doctor.py +0 -340
- package/src/devcouncil/cli/commands/evidence.py +0 -48
- package/src/devcouncil/cli/commands/go.py +0 -656
- package/src/devcouncil/cli/commands/handoff.py +0 -69
- package/src/devcouncil/cli/commands/hook.py +0 -237
- package/src/devcouncil/cli/commands/init.py +0 -289
- package/src/devcouncil/cli/commands/integrate.py +0 -1515
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -112
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -488
- package/src/devcouncil/cli/commands/prompt.py +0 -61
- package/src/devcouncil/cli/commands/repair.py +0 -89
- package/src/devcouncil/cli/commands/report.py +0 -137
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
- package/src/devcouncil/cli/commands/rollback.py +0 -59
- package/src/devcouncil/cli/commands/run.py +0 -289
- package/src/devcouncil/cli/commands/runs.py +0 -223
- package/src/devcouncil/cli/commands/scaffold.py +0 -32
- package/src/devcouncil/cli/commands/semantic.py +0 -47
- package/src/devcouncil/cli/commands/setup.py +0 -362
- package/src/devcouncil/cli/commands/shell.py +0 -73
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/skills.py +0 -88
- package/src/devcouncil/cli/commands/status.py +0 -141
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -88
- package/src/devcouncil/cli/commands/verify.py +0 -328
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/commands/watch_fs.py +0 -40
- package/src/devcouncil/cli/main.py +0 -137
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +0 -19
- package/src/devcouncil/council/prompts/critic_a.md +0 -10
- package/src/devcouncil/council/prompts/critic_b.md +0 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
- package/src/devcouncil/council/prompts/planner_a.md +0 -16
- package/src/devcouncil/council/prompts/planner_b.md +0 -16
- package/src/devcouncil/council/prompts/rebuttal.md +0 -10
- package/src/devcouncil/council/prompts/spec_writer.md +0 -12
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +0 -17
- package/src/devcouncil/domain/critique.py +0 -32
- package/src/devcouncil/domain/evidence.py +0 -47
- package/src/devcouncil/domain/gap.py +0 -52
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -55
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/checkpoints.py +0 -246
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/fs_watcher.py +0 -180
- package/src/devcouncil/execution/handoff.py +0 -102
- package/src/devcouncil/execution/hook_policy.py +0 -253
- package/src/devcouncil/execution/patch.py +0 -77
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -85
- package/src/devcouncil/execution/policy_engine.py +0 -343
- package/src/devcouncil/execution/prompt_builder.py +0 -671
- package/src/devcouncil/execution/shell_session.py +0 -225
- package/src/devcouncil/execution/task_runner.py +0 -170
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/agent_registry.py +0 -575
- package/src/devcouncil/executors/coding_cli.py +0 -736
- package/src/devcouncil/executors/mini_swe.py +0 -73
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +0 -208
- package/src/devcouncil/executors/openhands.py +0 -71
- package/src/devcouncil/gating/__init__.py +0 -1
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +0 -52
- package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
- package/src/devcouncil/gating/policy.py +0 -338
- package/src/devcouncil/hardware.py +0 -184
- package/src/devcouncil/indexing/__init__.py +0 -1
- package/src/devcouncil/indexing/ast_matcher.py +0 -168
- package/src/devcouncil/indexing/graph_index.py +0 -48
- package/src/devcouncil/indexing/lsp.py +0 -161
- package/src/devcouncil/indexing/repo_mapper.py +0 -1455
- package/src/devcouncil/indexing/semantic_index.py +0 -205
- package/src/devcouncil/integrations/actions.py +0 -146
- package/src/devcouncil/integrations/check.py +0 -423
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/github_intent.py +0 -142
- package/src/devcouncil/integrations/gitnexus.py +0 -62
- package/src/devcouncil/integrations/graphify.py +0 -34
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +0 -2122
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -349
- package/src/devcouncil/live/models.py +0 -63
- package/src/devcouncil/live/repair_prompt.py +0 -83
- package/src/devcouncil/live/reviewer.py +0 -70
- package/src/devcouncil/live/signals.py +0 -135
- package/src/devcouncil/live/summary.py +0 -34
- package/src/devcouncil/live/tasks.py +0 -18
- package/src/devcouncil/live/transcripts.py +0 -141
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -42
- package/src/devcouncil/llm/model_defaults.yaml +0 -44
- package/src/devcouncil/llm/provider.py +0 -627
- package/src/devcouncil/llm/router.py +0 -310
- package/src/devcouncil/optimization/__init__.py +0 -1
- package/src/devcouncil/optimization/gepa_agent.py +0 -318
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/correction_manifest.py +0 -303
- package/src/devcouncil/planning/critique_service.py +0 -71
- package/src/devcouncil/planning/plan_service.py +0 -60
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -70
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/repo/ci_scaffold.py +0 -157
- package/src/devcouncil/repo/gitignore.py +0 -123
- package/src/devcouncil/repo/sca.py +0 -374
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +0 -32
- package/src/devcouncil/reporting/json_report.py +0 -30
- package/src/devcouncil/reporting/markdown_report.py +0 -83
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/skills/__init__.py +0 -19
- package/src/devcouncil/skills/library/README.md +0 -46
- package/src/devcouncil/skills/library/ai-training.md +0 -50
- package/src/devcouncil/skills/library/android.md +0 -50
- package/src/devcouncil/skills/library/backend.md +0 -52
- package/src/devcouncil/skills/library/core-engineering.md +0 -95
- package/src/devcouncil/skills/library/data-engineering.md +0 -47
- package/src/devcouncil/skills/library/desktop.md +0 -46
- package/src/devcouncil/skills/library/devops.md +0 -48
- package/src/devcouncil/skills/library/game-dev.md +0 -46
- package/src/devcouncil/skills/library/ios.md +0 -48
- package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
- package/src/devcouncil/skills/library/security.md +0 -48
- package/src/devcouncil/skills/library/systems.md +0 -48
- package/src/devcouncil/skills/library/web.md +0 -47
- package/src/devcouncil/skills/library/windows.md +0 -47
- package/src/devcouncil/skills/registry.py +0 -330
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -147
- package/src/devcouncil/storage/models.py +0 -204
- package/src/devcouncil/storage/native.py +0 -557
- package/src/devcouncil/storage/repositories.py +0 -485
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -140
- package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
- package/src/devcouncil/telemetry/pricing.py +0 -28
- package/src/devcouncil/telemetry/traces.py +0 -146
- package/src/devcouncil/telemetry/tracker.py +0 -52
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -423
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -147
- package/src/devcouncil/utils/subprocess_env.py +0 -69
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/acceptance_compiler.py +0 -125
- package/src/devcouncil/verification/ad_hoc_check.py +0 -129
- package/src/devcouncil/verification/diff_coverage.py +0 -353
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/next_actions.py +0 -189
- package/src/devcouncil/verification/sandbox.py +0 -178
- package/src/devcouncil/verification/test_resolver.py +0 -91
- package/src/devcouncil/verification/verifier.py +0 -1548
- package/uv.lock +0 -1226
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
"""Shared task policy engine for shell commands and file changes."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import fnmatch
|
|
6
|
-
import re
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Literal
|
|
9
|
-
|
|
10
|
-
from pydantic import BaseModel
|
|
11
|
-
|
|
12
|
-
from devcouncil.domain.task import PlannedFile, Task
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class PolicyDecision(BaseModel):
|
|
16
|
-
action: Literal["allow", "warn", "deny"]
|
|
17
|
-
reason: str
|
|
18
|
-
target: str
|
|
19
|
-
task_id: str | None = None
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def normalize_repo_path(project_root: Path, raw_path: str) -> tuple[str, bool]:
|
|
23
|
-
"""Resolve ``raw_path`` against ``project_root`` and report containment.
|
|
24
|
-
|
|
25
|
-
Returns ``(normalized_posix_path, is_outside_root)``. This is the single source of
|
|
26
|
-
truth used by both the task policy engine and the coding-CLI hook policy, so a path
|
|
27
|
-
is normalized identically wherever it is checked — closing the bypass where a path
|
|
28
|
-
that failed to resolve was returned raw and enforced differently than it was
|
|
29
|
-
checked. ``is_outside_root`` is True for anything that escapes the project (absolute
|
|
30
|
-
elsewhere, ``..`` traversal) or cannot be resolved, so callers fail closed."""
|
|
31
|
-
cleaned = raw_path.strip().strip('"').replace("\\", "/")
|
|
32
|
-
root = project_root.resolve()
|
|
33
|
-
try:
|
|
34
|
-
candidate = Path(cleaned)
|
|
35
|
-
resolved = candidate.resolve() if candidate.is_absolute() else (root / cleaned).resolve()
|
|
36
|
-
except OSError:
|
|
37
|
-
fallback = cleaned[2:] if cleaned.startswith("./") else cleaned
|
|
38
|
-
return fallback, True
|
|
39
|
-
try:
|
|
40
|
-
return resolved.relative_to(root).as_posix(), False
|
|
41
|
-
except ValueError:
|
|
42
|
-
return resolved.as_posix(), True
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
_NO_TASK_ALLOWED_COMMANDS = (
|
|
46
|
-
"dev status",
|
|
47
|
-
"dev tasks",
|
|
48
|
-
"git status",
|
|
49
|
-
"git diff",
|
|
50
|
-
"git diff *",
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
# Shared with HookPolicy — keep these as the single source of truth for
|
|
54
|
-
# protected/secret path patterns.
|
|
55
|
-
PROTECTED_WRITE_PATTERNS = (
|
|
56
|
-
"package.json",
|
|
57
|
-
"pyproject.toml",
|
|
58
|
-
"uv.lock",
|
|
59
|
-
"package-lock.json",
|
|
60
|
-
"yarn.lock",
|
|
61
|
-
"pnpm-lock.yaml",
|
|
62
|
-
"Dockerfile",
|
|
63
|
-
"docker-compose.yml",
|
|
64
|
-
".github/workflows/*.yml",
|
|
65
|
-
".github/workflows/*.yaml",
|
|
66
|
-
"schema.prisma",
|
|
67
|
-
"wrangler.toml",
|
|
68
|
-
"index.html",
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
SECRET_PATH_PATTERNS = (
|
|
72
|
-
".env",
|
|
73
|
-
".env.*",
|
|
74
|
-
"**/.env",
|
|
75
|
-
"**/.env.*",
|
|
76
|
-
"**/credentials/**",
|
|
77
|
-
"**/secrets/**",
|
|
78
|
-
"**/*.pem",
|
|
79
|
-
"**/*.key",
|
|
80
|
-
# Private SSH keys and well-known credential/token files. These are never
|
|
81
|
-
# writable through the gate so an agent cannot plant or overwrite credentials.
|
|
82
|
-
"**/id_rsa",
|
|
83
|
-
"**/id_dsa",
|
|
84
|
-
"**/id_ecdsa",
|
|
85
|
-
"**/id_ed25519",
|
|
86
|
-
"id_rsa",
|
|
87
|
-
"id_ed25519",
|
|
88
|
-
".npmrc",
|
|
89
|
-
"**/.npmrc",
|
|
90
|
-
".pypirc",
|
|
91
|
-
"**/.pypirc",
|
|
92
|
-
".netrc",
|
|
93
|
-
"**/.netrc",
|
|
94
|
-
"**/.aws/credentials",
|
|
95
|
-
".aws/credentials",
|
|
96
|
-
"**/*.pfx",
|
|
97
|
-
"**/*.p12",
|
|
98
|
-
"*.pfx",
|
|
99
|
-
"*.p12",
|
|
100
|
-
".git-credentials",
|
|
101
|
-
"**/.git-credentials",
|
|
102
|
-
"**/kube/config",
|
|
103
|
-
"**/.kube/config",
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
_RESTRICTED_PATH_PATTERNS = (
|
|
107
|
-
".git/*",
|
|
108
|
-
".devcouncil/*",
|
|
109
|
-
# Protect the client hook/agent configs themselves: an agent must not be able to
|
|
110
|
-
# disarm or rewire the pre-action gate by editing its own client integration files.
|
|
111
|
-
".claude/*",
|
|
112
|
-
".claude/**",
|
|
113
|
-
".codex/*",
|
|
114
|
-
".codex/**",
|
|
115
|
-
".cursor/*",
|
|
116
|
-
".cursor/**",
|
|
117
|
-
".gemini/*",
|
|
118
|
-
".gemini/**",
|
|
119
|
-
".opencode/*",
|
|
120
|
-
".opencode/**",
|
|
121
|
-
".agents/*",
|
|
122
|
-
".agents/**",
|
|
123
|
-
"opencode.json",
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class TaskPolicyEngine:
|
|
128
|
-
def __init__(
|
|
129
|
-
self,
|
|
130
|
-
project_root: Path,
|
|
131
|
-
global_allowed_commands: list[str] | None = None,
|
|
132
|
-
):
|
|
133
|
-
self.project_root = project_root.resolve()
|
|
134
|
-
self.global_allowed_commands = global_allowed_commands or []
|
|
135
|
-
|
|
136
|
-
def evaluate_command(self, command: str, task: Task | None) -> PolicyDecision:
|
|
137
|
-
normalized = " ".join(command.split())
|
|
138
|
-
if not normalized:
|
|
139
|
-
return PolicyDecision(
|
|
140
|
-
action="deny",
|
|
141
|
-
reason="Empty command is not allowed.",
|
|
142
|
-
target=command,
|
|
143
|
-
task_id=task.id if task else None,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
if task is None:
|
|
147
|
-
if any(fnmatch.fnmatch(normalized, allowed) for allowed in _NO_TASK_ALLOWED_COMMANDS):
|
|
148
|
-
return PolicyDecision(
|
|
149
|
-
action="allow",
|
|
150
|
-
reason="Read-only command allowed without active task.",
|
|
151
|
-
target=normalized,
|
|
152
|
-
)
|
|
153
|
-
return PolicyDecision(
|
|
154
|
-
action="deny",
|
|
155
|
-
reason="Shell commands require an active task lease.",
|
|
156
|
-
target=normalized,
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
if any(fnmatch.fnmatch(normalized, allowed) for allowed in task.allowed_commands):
|
|
160
|
-
return PolicyDecision(
|
|
161
|
-
action="allow",
|
|
162
|
-
reason="Command matches task allowed_commands.",
|
|
163
|
-
target=normalized,
|
|
164
|
-
task_id=task.id,
|
|
165
|
-
)
|
|
166
|
-
if any(fnmatch.fnmatch(normalized, allowed) for allowed in self.global_allowed_commands):
|
|
167
|
-
return PolicyDecision(
|
|
168
|
-
action="allow",
|
|
169
|
-
reason="Command matches global allowed commands.",
|
|
170
|
-
target=normalized,
|
|
171
|
-
task_id=task.id,
|
|
172
|
-
)
|
|
173
|
-
return PolicyDecision(
|
|
174
|
-
action="deny",
|
|
175
|
-
reason="Command is not in task or global allowlists.",
|
|
176
|
-
target=normalized,
|
|
177
|
-
task_id=task.id,
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
def evaluate_file_change(
|
|
181
|
-
self,
|
|
182
|
-
path: str,
|
|
183
|
-
task: Task | None,
|
|
184
|
-
operation: Literal["create", "modify", "delete", "write"] = "write",
|
|
185
|
-
*,
|
|
186
|
-
internal: bool = False,
|
|
187
|
-
) -> PolicyDecision:
|
|
188
|
-
normalized, outside_root = normalize_repo_path(self.project_root, path)
|
|
189
|
-
task_id = task.id if task else None
|
|
190
|
-
|
|
191
|
-
if outside_root:
|
|
192
|
-
return PolicyDecision(
|
|
193
|
-
action="deny",
|
|
194
|
-
reason="Path is outside the project root.",
|
|
195
|
-
target=normalized,
|
|
196
|
-
task_id=task_id,
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
if self._matches_any(normalized, SECRET_PATH_PATTERNS):
|
|
200
|
-
return PolicyDecision(
|
|
201
|
-
action="deny",
|
|
202
|
-
reason="Secret and credential paths are never writable.",
|
|
203
|
-
target=normalized,
|
|
204
|
-
task_id=task_id,
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
if not internal and self._matches_restricted(normalized):
|
|
208
|
-
return PolicyDecision(
|
|
209
|
-
action="deny",
|
|
210
|
-
reason="Protected repository paths cannot be modified.",
|
|
211
|
-
target=normalized,
|
|
212
|
-
task_id=task_id,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
if task is None:
|
|
216
|
-
return PolicyDecision(
|
|
217
|
-
action="deny",
|
|
218
|
-
reason="No running DevCouncil task authorizes this file write.",
|
|
219
|
-
target=normalized,
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
if self._matches_forbidden(normalized, task):
|
|
223
|
-
return PolicyDecision(
|
|
224
|
-
action="deny",
|
|
225
|
-
reason="Path is listed in forbidden_changes.",
|
|
226
|
-
target=normalized,
|
|
227
|
-
task_id=task.id,
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
planned = self._planned_file_for(normalized, task)
|
|
231
|
-
if planned is None:
|
|
232
|
-
return PolicyDecision(
|
|
233
|
-
action="deny",
|
|
234
|
-
reason=f"Task {task.id} does not authorize changes to {normalized}.",
|
|
235
|
-
target=normalized,
|
|
236
|
-
task_id=task.id,
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
if planned.allowed_change == "read_only":
|
|
240
|
-
return PolicyDecision(
|
|
241
|
-
action="deny",
|
|
242
|
-
reason="Planned file is read-only.",
|
|
243
|
-
target=normalized,
|
|
244
|
-
task_id=task.id,
|
|
245
|
-
)
|
|
246
|
-
if operation == "write":
|
|
247
|
-
if planned.allowed_change not in {"create", "modify"}:
|
|
248
|
-
return PolicyDecision(
|
|
249
|
-
action="deny",
|
|
250
|
-
reason=f"Operation {operation} not allowed for planned file.",
|
|
251
|
-
target=normalized,
|
|
252
|
-
task_id=task.id,
|
|
253
|
-
)
|
|
254
|
-
elif planned.allowed_change != operation:
|
|
255
|
-
return PolicyDecision(
|
|
256
|
-
action="deny",
|
|
257
|
-
reason=f"Operation {operation} not allowed for planned file.",
|
|
258
|
-
target=normalized,
|
|
259
|
-
task_id=task.id,
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
if self._matches_any(normalized, PROTECTED_WRITE_PATTERNS):
|
|
263
|
-
return PolicyDecision(
|
|
264
|
-
action="warn",
|
|
265
|
-
reason=f"{normalized} is a protected high-impact file; verification gates must approve it.",
|
|
266
|
-
target=normalized,
|
|
267
|
-
task_id=task.id,
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
return PolicyDecision(
|
|
271
|
-
action="allow",
|
|
272
|
-
reason="File change is allowed.",
|
|
273
|
-
target=normalized,
|
|
274
|
-
task_id=task.id,
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
def evaluate_hook_command(self, command: str) -> PolicyDecision:
|
|
278
|
-
"""Git safety checks for hook/shell tool paths."""
|
|
279
|
-
normalized = " ".join(command.split())
|
|
280
|
-
lowered = normalized.lower()
|
|
281
|
-
if not normalized:
|
|
282
|
-
return PolicyDecision(action="allow", reason="No command detected.", target=normalized)
|
|
283
|
-
|
|
284
|
-
if "--no-verify" in lowered or "--no-gpg-sign" in lowered:
|
|
285
|
-
return PolicyDecision(
|
|
286
|
-
action="deny",
|
|
287
|
-
reason="Verification bypass flags are not allowed.",
|
|
288
|
-
target=normalized,
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
if re.search(r"\bgit\s+reset\s+--hard\s+(origin/)?(main|master)\b", lowered):
|
|
292
|
-
return PolicyDecision(
|
|
293
|
-
action="deny",
|
|
294
|
-
reason="Protected branch hard resets are not allowed.",
|
|
295
|
-
target=normalized,
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
if re.search(r"\bgit\s+push\b.*(\s--force(?:-with-lease)?\b|\s-f\b)", lowered) or re.search(
|
|
299
|
-
r"\bgit\s+push\s+\S+\s+\+\S", lowered
|
|
300
|
-
):
|
|
301
|
-
# The second pattern catches the leading-plus refspec form
|
|
302
|
-
# (`git push origin +HEAD:master`), which forces a non-fast-forward update
|
|
303
|
-
# without the --force flag.
|
|
304
|
-
return PolicyDecision(
|
|
305
|
-
action="deny",
|
|
306
|
-
reason="Force pushes are not allowed.",
|
|
307
|
-
target=normalized,
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
if re.search(r"\bgit\s+push\s+\S+\s+((head:)?(main|master)|(main|master):\S+)\b", lowered):
|
|
311
|
-
return PolicyDecision(
|
|
312
|
-
action="warn",
|
|
313
|
-
reason="Direct pushes to protected branches should go through verification gates.",
|
|
314
|
-
target=normalized,
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
return PolicyDecision(action="allow", reason="Command is allowed.", target=normalized)
|
|
318
|
-
|
|
319
|
-
def _normalize_path(self, raw_path: str) -> str:
|
|
320
|
-
return normalize_repo_path(self.project_root, raw_path)[0]
|
|
321
|
-
|
|
322
|
-
def _planned_file_for(self, path: str, task: Task) -> PlannedFile | None:
|
|
323
|
-
for planned in task.planned_files:
|
|
324
|
-
planned_path = planned.path.replace("\\", "/")
|
|
325
|
-
if path == planned_path or fnmatch.fnmatch(path, planned_path):
|
|
326
|
-
return planned
|
|
327
|
-
return None
|
|
328
|
-
|
|
329
|
-
def _matches_forbidden(self, path: str, task: Task) -> bool:
|
|
330
|
-
for forbidden in task.forbidden_changes:
|
|
331
|
-
pattern = forbidden.replace("\\", "/")
|
|
332
|
-
if path == pattern or fnmatch.fnmatch(path, pattern):
|
|
333
|
-
return True
|
|
334
|
-
return False
|
|
335
|
-
|
|
336
|
-
def _matches_restricted(self, path: str) -> bool:
|
|
337
|
-
for pattern in _RESTRICTED_PATH_PATTERNS:
|
|
338
|
-
if fnmatch.fnmatch(path, pattern) or path.startswith(pattern.strip("*")):
|
|
339
|
-
return True
|
|
340
|
-
return False
|
|
341
|
-
|
|
342
|
-
def _matches_any(self, path: str, patterns: tuple[str, ...]) -> bool:
|
|
343
|
-
return any(fnmatch.fnmatch(path, pattern) for pattern in patterns)
|