agent-rules-kit 0.2.1__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.
- agent_rules_kit/__init__.py +3 -0
- agent_rules_kit/cli.py +345 -0
- agent_rules_kit/discovery.py +109 -0
- agent_rules_kit/findings.py +85 -0
- agent_rules_kit/governance.py +608 -0
- agent_rules_kit/init_plan.py +73 -0
- agent_rules_kit/init_write.py +143 -0
- agent_rules_kit/redaction.py +78 -0
- agent_rules_kit-0.2.1.dist-info/METADATA +613 -0
- agent_rules_kit-0.2.1.dist-info/RECORD +13 -0
- agent_rules_kit-0.2.1.dist-info/WHEEL +4 -0
- agent_rules_kit-0.2.1.dist-info/entry_points.txt +2 -0
- agent_rules_kit-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Explicit write mode for agent instruction init."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from agent_rules_kit.init_plan import InitPlanAction, build_init_plan
|
|
10
|
+
|
|
11
|
+
BASELINE_AGENTS_CONTENT = """# Agent Instructions
|
|
12
|
+
|
|
13
|
+
These baseline instructions were generated by agent-rules-kit.
|
|
14
|
+
Review and adapt them before relying on them for a specific project.
|
|
15
|
+
|
|
16
|
+
## Scope
|
|
17
|
+
|
|
18
|
+
These instructions apply from the repository root unless a more specific instruction file
|
|
19
|
+
defines a narrower scope.
|
|
20
|
+
|
|
21
|
+
## Authority
|
|
22
|
+
|
|
23
|
+
Follow explicit user requests first. Then follow this file and any more specific
|
|
24
|
+
instruction files for the files being changed.
|
|
25
|
+
|
|
26
|
+
## Secret handling
|
|
27
|
+
|
|
28
|
+
Do not commit, print, or log secrets, tokens, credentials, API keys, private URLs, or private data.
|
|
29
|
+
|
|
30
|
+
## Command execution
|
|
31
|
+
|
|
32
|
+
Read the repository before changing files.
|
|
33
|
+
Ask for explicit confirmation before destructive commands or broad filesystem changes.
|
|
34
|
+
|
|
35
|
+
## Review and CI
|
|
36
|
+
|
|
37
|
+
Keep the repository review process, CI requirements, branch protection, and maintainer
|
|
38
|
+
approval intact.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class WrittenInitFile:
|
|
44
|
+
"""A file action performed by init write mode."""
|
|
45
|
+
|
|
46
|
+
path: str
|
|
47
|
+
action: InitPlanAction
|
|
48
|
+
backup_path: str | None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class InitWriteResult:
|
|
53
|
+
"""Result of explicit init write mode."""
|
|
54
|
+
|
|
55
|
+
repository: str
|
|
56
|
+
files: tuple[WrittenInitFile, ...]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write_init_files(root: Path | str) -> InitWriteResult:
|
|
60
|
+
"""Write baseline init files, backing up existing files before replacement."""
|
|
61
|
+
plan = build_init_plan(root)
|
|
62
|
+
root_path = Path(root)
|
|
63
|
+
written_files: list[WrittenInitFile] = []
|
|
64
|
+
|
|
65
|
+
for planned_file in plan.files:
|
|
66
|
+
target = root_path / planned_file.path
|
|
67
|
+
backup_path: Path | None = None
|
|
68
|
+
|
|
69
|
+
if target.is_symlink():
|
|
70
|
+
raise ValueError("refusing to write init file through symlinked path: AGENTS.md")
|
|
71
|
+
|
|
72
|
+
if planned_file.action == InitPlanAction.BACKUP_AND_REPLACE:
|
|
73
|
+
backup_path = _next_backup_path(target)
|
|
74
|
+
_copy_file_to_new_regular_path(target, backup_path)
|
|
75
|
+
|
|
76
|
+
_write_text_atomic(target, BASELINE_AGENTS_CONTENT)
|
|
77
|
+
|
|
78
|
+
written_files.append(
|
|
79
|
+
WrittenInitFile(
|
|
80
|
+
path=planned_file.path,
|
|
81
|
+
action=planned_file.action,
|
|
82
|
+
backup_path=(
|
|
83
|
+
backup_path.relative_to(root_path).as_posix()
|
|
84
|
+
if backup_path is not None
|
|
85
|
+
else None
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return InitWriteResult(
|
|
91
|
+
repository=plan.repository,
|
|
92
|
+
files=tuple(written_files),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _write_text_atomic(target: Path, content: str) -> None:
|
|
97
|
+
temporary_path = _next_available_path(
|
|
98
|
+
target.with_name(f".{target.name}.agent-rules-kit.tmp")
|
|
99
|
+
)
|
|
100
|
+
temporary_created = False
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with temporary_path.open("x", encoding="utf-8") as temporary_file:
|
|
104
|
+
temporary_created = True
|
|
105
|
+
temporary_file.write(content)
|
|
106
|
+
temporary_path.replace(target)
|
|
107
|
+
finally:
|
|
108
|
+
if temporary_created and _path_exists_or_is_symlink(temporary_path):
|
|
109
|
+
temporary_path.unlink()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _copy_file_to_new_regular_path(source: Path, destination: Path) -> None:
|
|
113
|
+
with source.open("rb") as source_file, destination.open("xb") as destination_file:
|
|
114
|
+
shutil.copyfileobj(source_file, destination_file)
|
|
115
|
+
shutil.copystat(source, destination, follow_symlinks=False)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _next_backup_path(target: Path) -> Path:
|
|
119
|
+
return _next_available_path(target.with_name(f"{target.name}.agent-rules-kit.bak"))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _next_available_path(candidate: Path) -> Path:
|
|
123
|
+
if not _path_exists_or_is_symlink(candidate):
|
|
124
|
+
return candidate
|
|
125
|
+
|
|
126
|
+
for index in range(1, 1000):
|
|
127
|
+
indexed_candidate = candidate.with_name(f"{candidate.name}.{index}")
|
|
128
|
+
if not _path_exists_or_is_symlink(indexed_candidate):
|
|
129
|
+
return indexed_candidate
|
|
130
|
+
|
|
131
|
+
raise RuntimeError(f"could not find available backup path for: {candidate}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _path_exists_or_is_symlink(candidate: Path) -> bool:
|
|
135
|
+
return candidate.exists() or candidate.is_symlink()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
__all__ = [
|
|
139
|
+
"BASELINE_AGENTS_CONTENT",
|
|
140
|
+
"InitWriteResult",
|
|
141
|
+
"WrittenInitFile",
|
|
142
|
+
"write_init_files",
|
|
143
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Redaction helpers for secret-like values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from re import Pattern
|
|
8
|
+
|
|
9
|
+
REDACTION_TEXT = "[REDACTED]"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class RedactionPattern:
|
|
14
|
+
"""A named pattern used to redact secret-like values."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
pattern: Pattern[str]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
SECRET_LIKE_PATTERNS: tuple[RedactionPattern, ...] = (
|
|
21
|
+
RedactionPattern(
|
|
22
|
+
name="anthropic_api_key",
|
|
23
|
+
pattern=re.compile(r"sk-ant-[A-Za-z0-9_-]{12,}"),
|
|
24
|
+
),
|
|
25
|
+
RedactionPattern(
|
|
26
|
+
name="openai_api_key",
|
|
27
|
+
pattern=re.compile(r"sk-[A-Za-z0-9_-]{12,}"),
|
|
28
|
+
),
|
|
29
|
+
RedactionPattern(
|
|
30
|
+
name="jwt_token",
|
|
31
|
+
pattern=re.compile(r"eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}"),
|
|
32
|
+
),
|
|
33
|
+
RedactionPattern(
|
|
34
|
+
name="github_token",
|
|
35
|
+
pattern=re.compile(r"gh[pousr]_[A-Za-z0-9_]{20,}"),
|
|
36
|
+
),
|
|
37
|
+
RedactionPattern(
|
|
38
|
+
name="aws_access_key",
|
|
39
|
+
pattern=re.compile(r"AKIA[0-9A-Z]{16}"),
|
|
40
|
+
),
|
|
41
|
+
RedactionPattern(
|
|
42
|
+
name="huggingface_token",
|
|
43
|
+
pattern=re.compile(r"hf_[A-Za-z0-9]{20,}"),
|
|
44
|
+
),
|
|
45
|
+
RedactionPattern(
|
|
46
|
+
name="slack_token",
|
|
47
|
+
pattern=re.compile(r"xox[bpsa]-[A-Za-z0-9-]{10,}"),
|
|
48
|
+
),
|
|
49
|
+
RedactionPattern(
|
|
50
|
+
name="npm_token",
|
|
51
|
+
pattern=re.compile(r"npm_[A-Za-z0-9]{20,}"),
|
|
52
|
+
),
|
|
53
|
+
RedactionPattern(
|
|
54
|
+
name="private_key_block",
|
|
55
|
+
pattern=re.compile(
|
|
56
|
+
r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----",
|
|
57
|
+
re.DOTALL,
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def redact_secret_like_values(text: str) -> str:
|
|
64
|
+
"""Redact supported secret-like values from text."""
|
|
65
|
+
redacted = text
|
|
66
|
+
|
|
67
|
+
for item in SECRET_LIKE_PATTERNS:
|
|
68
|
+
redacted = item.pattern.sub(REDACTION_TEXT, redacted)
|
|
69
|
+
|
|
70
|
+
return redacted
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"REDACTION_TEXT",
|
|
75
|
+
"RedactionPattern",
|
|
76
|
+
"SECRET_LIKE_PATTERNS",
|
|
77
|
+
"redact_secret_like_values",
|
|
78
|
+
]
|