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.
@@ -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
+ ]