aepkit 0.6.0__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.
- aepkit/__init__.py +1 -0
- aepkit/__main__.py +7 -0
- aepkit/cli.py +151 -0
- aepkit/install_skill.py +126 -0
- aepkit/lint.py +549 -0
- aepkit/negcontrol.py +234 -0
- aepkit/py.typed +0 -0
- aepkit/scaffold.py +40 -0
- aepkit/skill/SKILL.md +448 -0
- aepkit/skill/references/autonomous-mode.md +144 -0
- aepkit/skill/references/failure-modes-and-example.md +103 -0
- aepkit/skill/references/handoffs.md +133 -0
- aepkit/skill/references/tasks-and-gates.md +84 -0
- aepkit/status.py +122 -0
- aepkit/templates/compliance-log.md +37 -0
- aepkit/templates/execution-guide.md +75 -0
- aepkit/templates/plan-of-record.md +79 -0
- aepkit-0.6.0.dist-info/METADATA +223 -0
- aepkit-0.6.0.dist-info/RECORD +23 -0
- aepkit-0.6.0.dist-info/WHEEL +5 -0
- aepkit-0.6.0.dist-info/entry_points.txt +2 -0
- aepkit-0.6.0.dist-info/licenses/LICENSE +21 -0
- aepkit-0.6.0.dist-info/top_level.txt +1 -0
aepkit/negcontrol.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Run the backup -> break -> test (expect fail) -> restore -> test (expect pass)
|
|
2
|
+
sequence this protocol requires for every gate that guards a security, privacy, or
|
|
3
|
+
financial-integrity boundary, and print a paste-ready transcript.
|
|
4
|
+
|
|
5
|
+
This replaces doing the same four shell commands by hand, which is where the sequence
|
|
6
|
+
tends to get shortened under time pressure.
|
|
7
|
+
|
|
8
|
+
With --file, the backup is the source of truth: whatever --restore-cmd does, the file
|
|
9
|
+
must end byte-identical to the backup, or it is restored from the backup. The backup is
|
|
10
|
+
deleted only once the file is verified identical; otherwise its path is printed.
|
|
11
|
+
|
|
12
|
+
A failure only counts if it is the right failure. With --expect, the output of the
|
|
13
|
+
failing run must match that regular expression, so a syntax error, a missing import or a
|
|
14
|
+
typo in the test command cannot pass for the protection being caught. Without it, output
|
|
15
|
+
that looks like such an error is flagged in the verdict.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import filecmp
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import shutil
|
|
24
|
+
import signal
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
TIMEOUT_EXIT = 124
|
|
31
|
+
|
|
32
|
+
# Output that usually means the test never ran, rather than ran and caught the break.
|
|
33
|
+
_CRASH_SIGNATURES = re.compile(
|
|
34
|
+
r"SyntaxError|IndentationError|ImportError|ModuleNotFoundError|ERROR collecting"
|
|
35
|
+
r"|Cannot find module|command not found|is not recognized as an internal or external command"
|
|
36
|
+
r"|^\(timed out after ",
|
|
37
|
+
re.MULTILINE,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _kill_tree(proc: subprocess.Popen[str]) -> None:
|
|
42
|
+
"""Kill the shell and every process it started, not only the shell."""
|
|
43
|
+
if os.name == "nt":
|
|
44
|
+
subprocess.run(
|
|
45
|
+
["taskkill", "/PID", str(proc.pid), "/T", "/F"], capture_output=True, check=False
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
try:
|
|
49
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
50
|
+
except ProcessLookupError:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _run(cmd: str, timeout: float | None = None) -> tuple[int, str]:
|
|
55
|
+
# Each command runs in its own process group (session), so a timeout can kill the
|
|
56
|
+
# processes it started: a timed-out break must not keep changing the file afterwards.
|
|
57
|
+
if os.name == "nt":
|
|
58
|
+
group = {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP} # type: ignore[attr-defined]
|
|
59
|
+
else:
|
|
60
|
+
group = {"start_new_session": True}
|
|
61
|
+
proc = subprocess.Popen(
|
|
62
|
+
cmd,
|
|
63
|
+
shell=True,
|
|
64
|
+
stdout=subprocess.PIPE,
|
|
65
|
+
stderr=subprocess.STDOUT,
|
|
66
|
+
text=True,
|
|
67
|
+
**group,
|
|
68
|
+
)
|
|
69
|
+
try:
|
|
70
|
+
output, _ = proc.communicate(timeout=timeout)
|
|
71
|
+
except subprocess.TimeoutExpired:
|
|
72
|
+
_kill_tree(proc)
|
|
73
|
+
proc.communicate()
|
|
74
|
+
return TIMEOUT_EXIT, f"(timed out after {timeout:g}s)"
|
|
75
|
+
return proc.returncode, output or ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _is_identical(file_path: str, backup_path: Path) -> bool:
|
|
79
|
+
try:
|
|
80
|
+
return Path(file_path).is_file() and filecmp.cmp(file_path, str(backup_path), shallow=False)
|
|
81
|
+
except OSError:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _ensure_restored(
|
|
86
|
+
file_path: str, backup_path: Path, transcript: list[str], after_restore_cmd: bool
|
|
87
|
+
) -> bool:
|
|
88
|
+
"""Leave file_path byte-identical to the backup, whatever the break or restore did
|
|
89
|
+
(modified, deleted, moved). Returns False only if that could not be achieved."""
|
|
90
|
+
if _is_identical(file_path, backup_path):
|
|
91
|
+
transcript.append(f"$ {file_path} is byte-identical to the backup")
|
|
92
|
+
return True
|
|
93
|
+
if after_restore_cmd:
|
|
94
|
+
transcript.append("WARNING: --restore-cmd did not restore the file; using the backup.")
|
|
95
|
+
try:
|
|
96
|
+
target = Path(file_path)
|
|
97
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
shutil.copy2(backup_path, target)
|
|
99
|
+
except OSError as error:
|
|
100
|
+
transcript.append(f"ERROR: could not restore {file_path} from the backup: {error}")
|
|
101
|
+
return False
|
|
102
|
+
identical = _is_identical(file_path, backup_path)
|
|
103
|
+
transcript.append(
|
|
104
|
+
f"$ restored {file_path} from backup "
|
|
105
|
+
+ ("(byte-identical)" if identical else "- WARNING: still differs from backup")
|
|
106
|
+
)
|
|
107
|
+
return identical
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def run(
|
|
111
|
+
test_cmd: str,
|
|
112
|
+
*,
|
|
113
|
+
break_cmd: str | None = None,
|
|
114
|
+
file_path: str | None = None,
|
|
115
|
+
restore_cmd: str | None = None,
|
|
116
|
+
timeout: float | None = None,
|
|
117
|
+
expect: str | None = None,
|
|
118
|
+
) -> int:
|
|
119
|
+
expect_re: re.Pattern[str] | None = None
|
|
120
|
+
if expect is not None:
|
|
121
|
+
try:
|
|
122
|
+
expect_re = re.compile(expect, re.MULTILINE)
|
|
123
|
+
except re.error as error:
|
|
124
|
+
print(f"error: --expect is not a valid regular expression: {error}", file=sys.stderr)
|
|
125
|
+
return 2
|
|
126
|
+
if not break_cmd and not restore_cmd:
|
|
127
|
+
print(
|
|
128
|
+
"error: pass --break-cmd, or --restore-cmd for a break you apply yourself.",
|
|
129
|
+
file=sys.stderr,
|
|
130
|
+
)
|
|
131
|
+
return 2
|
|
132
|
+
if not file_path and not restore_cmd:
|
|
133
|
+
print(
|
|
134
|
+
"error: without --file, --restore-cmd is required (nothing to copy back).",
|
|
135
|
+
file=sys.stderr,
|
|
136
|
+
)
|
|
137
|
+
return 2
|
|
138
|
+
|
|
139
|
+
tmp_dir: Path | None = None
|
|
140
|
+
backup_path: Path | None = None
|
|
141
|
+
transcript: list[str] = []
|
|
142
|
+
# With --file, nothing counts as restored until the file is verified identical.
|
|
143
|
+
restored_ok = not file_path
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
if file_path:
|
|
147
|
+
src = Path(file_path)
|
|
148
|
+
if not src.exists():
|
|
149
|
+
print(f"error: {file_path} does not exist.", file=sys.stderr)
|
|
150
|
+
return 2
|
|
151
|
+
tmp_dir = Path(tempfile.mkdtemp(prefix="aepkit-negcontrol-"))
|
|
152
|
+
backup_path = tmp_dir / src.name
|
|
153
|
+
shutil.copy2(src, backup_path)
|
|
154
|
+
transcript.append(f"$ backed up {file_path} -> {backup_path}")
|
|
155
|
+
|
|
156
|
+
broke_as_expected = False
|
|
157
|
+
out1 = ""
|
|
158
|
+
try:
|
|
159
|
+
if break_cmd:
|
|
160
|
+
transcript.append(f"$ {break_cmd}")
|
|
161
|
+
code, out = _run(break_cmd, timeout)
|
|
162
|
+
transcript.append(out.rstrip("\n"))
|
|
163
|
+
if code != 0:
|
|
164
|
+
print(
|
|
165
|
+
f"warning: break command exited {code}. "
|
|
166
|
+
"Continuing, but check it did what you meant.",
|
|
167
|
+
file=sys.stderr,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
expect_note = f" matching /{expect}/" if expect is not None else ""
|
|
171
|
+
transcript.append(f"$ {test_cmd} # expect FAILURE{expect_note}")
|
|
172
|
+
code1, out1 = _run(test_cmd, timeout)
|
|
173
|
+
transcript.append(out1.rstrip("\n"))
|
|
174
|
+
broke_as_expected = code1 != 0
|
|
175
|
+
transcript.append(f"(exit {code1})")
|
|
176
|
+
finally:
|
|
177
|
+
if restore_cmd:
|
|
178
|
+
transcript.append(f"$ {restore_cmd}")
|
|
179
|
+
restore_code, out = _run(restore_cmd, timeout)
|
|
180
|
+
transcript.append(out.rstrip("\n"))
|
|
181
|
+
transcript.append(f"(restore exit {restore_code})")
|
|
182
|
+
restored_ok = restore_code == 0
|
|
183
|
+
if file_path and backup_path is not None:
|
|
184
|
+
restored_ok = _ensure_restored(
|
|
185
|
+
file_path, backup_path, transcript, after_restore_cmd=bool(restore_cmd)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
transcript.append(f"$ {test_cmd} # expect SUCCESS")
|
|
189
|
+
code2, out2 = _run(test_cmd, timeout)
|
|
190
|
+
transcript.append(out2.rstrip("\n"))
|
|
191
|
+
restored_to_green = code2 == 0
|
|
192
|
+
transcript.append(f"(exit {code2})")
|
|
193
|
+
|
|
194
|
+
print("\n".join(transcript))
|
|
195
|
+
print()
|
|
196
|
+
|
|
197
|
+
if not restored_ok:
|
|
198
|
+
print("FAIL: the protection was not restored. Check the tree before continuing.")
|
|
199
|
+
return 1
|
|
200
|
+
if not broke_as_expected:
|
|
201
|
+
print(
|
|
202
|
+
"FAIL: the test did not fail when the protection was removed. "
|
|
203
|
+
"Either the break command didn't do what you meant, or the protection isn't real."
|
|
204
|
+
)
|
|
205
|
+
return 1
|
|
206
|
+
if expect_re is not None and not expect_re.search(out1):
|
|
207
|
+
print(
|
|
208
|
+
f"FAIL: the test failed, but its output does not match --expect /{expect}/. "
|
|
209
|
+
"It may have failed for an unrelated reason (a syntax error, a missing import, "
|
|
210
|
+
"a wrong command); read the output above."
|
|
211
|
+
)
|
|
212
|
+
return 1
|
|
213
|
+
if not restored_to_green:
|
|
214
|
+
print("FAIL: the test did not return to passing after restore. The tree may be dirty.")
|
|
215
|
+
return 1
|
|
216
|
+
|
|
217
|
+
print(
|
|
218
|
+
"OK: negative control verified — the check fails without the protection and passes with it."
|
|
219
|
+
)
|
|
220
|
+
if expect_re is None:
|
|
221
|
+
crash = _CRASH_SIGNATURES.search(out1)
|
|
222
|
+
if crash:
|
|
223
|
+
print(
|
|
224
|
+
f"WARNING: the failing output contains {crash.group(0).strip()!r}, which "
|
|
225
|
+
"usually means the test did not run rather than caught the break. Read the "
|
|
226
|
+
"output above, and pass --expect with the failure you mean to see."
|
|
227
|
+
)
|
|
228
|
+
return 0
|
|
229
|
+
finally:
|
|
230
|
+
if tmp_dir and tmp_dir.exists():
|
|
231
|
+
if restored_ok:
|
|
232
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
233
|
+
else:
|
|
234
|
+
print(f"Backup kept at {backup_path}", file=sys.stderr)
|
aepkit/py.typed
ADDED
|
File without changes
|
aepkit/scaffold.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Scaffold the document set: three documents and an annexes/ directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
8
|
+
|
|
9
|
+
DOCUMENTS = ("plan-of-record.md", "execution-guide.md", "compliance-log.md")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(target_dir: str, project_name: str | None = None, force: bool = False) -> int:
|
|
13
|
+
target = Path(target_dir)
|
|
14
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
(target / "annexes").mkdir(exist_ok=True)
|
|
16
|
+
|
|
17
|
+
name = project_name or target.resolve().name
|
|
18
|
+
|
|
19
|
+
wrote = []
|
|
20
|
+
skipped = []
|
|
21
|
+
for doc in DOCUMENTS:
|
|
22
|
+
src = TEMPLATES_DIR / doc
|
|
23
|
+
dst = target / doc
|
|
24
|
+
if dst.exists() and not force:
|
|
25
|
+
skipped.append(dst)
|
|
26
|
+
continue
|
|
27
|
+
content = src.read_text(encoding="utf-8").replace("{{PROJECT_NAME}}", name)
|
|
28
|
+
dst.write_text(content, encoding="utf-8")
|
|
29
|
+
wrote.append(dst)
|
|
30
|
+
|
|
31
|
+
for path in wrote:
|
|
32
|
+
print(f"wrote {path}")
|
|
33
|
+
for path in skipped:
|
|
34
|
+
print(f"skip {path} (exists, use --force to overwrite)")
|
|
35
|
+
|
|
36
|
+
if not (target / "annexes" / ".gitkeep").exists():
|
|
37
|
+
(target / "annexes" / ".gitkeep").write_text("", encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
print(f"\n{target}/ scaffolded. Read execution-guide.md's Rules of engagement first.")
|
|
40
|
+
return 0
|