formwork-kit 0.1.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.
- formwork_cli/__init__.py +326 -0
- formwork_cli/kit/COSTS.md +111 -0
- formwork_cli/kit/adapters/claude-code/README.md +53 -0
- formwork_cli/kit/adapters/claude-code/settings.json +46 -0
- formwork_cli/kit/adapters/codex/README.md +43 -0
- formwork_cli/kit/adapters/cursor/README.md +45 -0
- formwork_cli/kit/adapters/gemini-cli/README.md +47 -0
- formwork_cli/kit/build +410 -0
- formwork_cli/kit/check/checks/config-shape +123 -0
- formwork_cli/kit/check/checks/decision-ids +159 -0
- formwork_cli/kit/check/checks/doc-links +133 -0
- formwork_cli/kit/check/checks/generated-current +74 -0
- formwork_cli/kit/check/checks/guard-wired +139 -0
- formwork_cli/kit/check/checks/kit-integrity +199 -0
- formwork_cli/kit/check/checks/predictions-first +127 -0
- formwork_cli/kit/check/checks/role-shape +172 -0
- formwork_cli/kit/check/checks/rule-labels +135 -0
- formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/.formwork.toml +5 -0
- formwork_cli/kit/check/fixtures/config-shape/must-fail/documents-a-section-that-does-not-exist/formwork/guide.md +13 -0
- formwork_cli/kit/check/fixtures/config-shape/must-fail/rules-as-a-switchboard/.formwork.toml +8 -0
- formwork_cli/kit/check/fixtures/config-shape/must-pass/layers-kept-apart/.formwork.toml +5 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/a-placeholder-shipped/docs/decisions/0003-still-pending.md +7 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/superseded-by-nothing/docs/decisions/0002-old.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-first.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-fail/two-decisions-one-number/docs/decisions/0007-second.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0001-the-first.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0002-the-second.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/clean-numbering/docs/decisions/0003-the-third.md +6 -0
- formwork_cli/kit/check/fixtures/decision-ids/must-pass/nothing-recorded-yet/docs/decisions/README.md +3 -0
- formwork_cli/kit/check/fixtures/doc-links/must-fail/never-written/index.md +7 -0
- formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/architecture-notes.md +3 -0
- formwork_cli/kit/check/fixtures/doc-links/must-fail/renamed-file/guide.md +8 -0
- formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/architecture-notes.md +1 -0
- formwork_cli/kit/check/fixtures/doc-links/must-pass/links-resolve/guide.md +5 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.claude/agents/sample.md +22 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.codex/agents/sample.toml +22 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/.gemini/agents/sample.md +23 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/build +349 -0
- formwork_cli/kit/check/fixtures/generated-current/must-fail/a-generated-file-was-edited/formwork/roles/method/sample.md +18 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.claude/agents/sample.md +20 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.codex/agents/sample.toml +22 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/.gemini/agents/sample.md +23 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/build +349 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/generated-and-current/formwork/roles/method/sample.md +18 -0
- formwork_cli/kit/check/fixtures/generated-current/must-pass/nothing-is-generated-here/README.md +3 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-fail/declared-but-no-file/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.claude/settings.json +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-fail/file-but-not-wired/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.claude/settings.json +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-pass/declared-and-wired/.formwork.toml +1 -0
- formwork_cli/kit/check/fixtures/guard-wired/must-pass/nothing-declared/README.md +1 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/formwork/check/checks/still-here +2 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-check-went-missing/state/fingerprints.txt +2 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/formwork/guard/git-boundary +3 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-fail/a-guard-was-altered/state/fingerprints.txt +1 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/formwork/guard/git-boundary +2 -0
- formwork_cli/kit/check/fixtures/kit-integrity/must-pass/everything-matches/state/fingerprints.txt +1 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/architect.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/researcher.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-fail/argued-with-no-predictions/docs/rounds/0004-the-storage-question/round.md +4 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/a-round-that-has-not-argued-yet/docs/rounds/0006-not-started/round.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/no-rounds-at-all/docs/README.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/architect.md +3 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/predictions.md +4 -0
- formwork_cli/kit/check/fixtures/predictions-first/must-pass/predictions-on-record/docs/rounds/0005-the-shape-of-a-brief/researcher.md +3 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/README.md +6 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/claims-a-grant-binds-everywhere/formwork/roles/complete.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/missing-a-section/formwork/roles/vague.md +16 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/spawn-without-being-lead/formwork/roles/eager.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/first.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-fail/two-roles-one-job/formwork/roles/second.md +18 -0
- formwork_cli/kit/check/fixtures/role-shape/must-pass/well-formed/formwork/roles/complete.md +18 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-fail/claims-enforcement-that-does-not-exist/formwork/rules/core.md +9 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-fail/no-catches/formwork/rules/core.md +9 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-fail/unlabelled/formwork/rules/core.md +7 -0
- formwork_cli/kit/check/fixtures/rule-labels/must-pass/well-formed/formwork/rules/core.md +10 -0
- formwork_cli/kit/check/run +340 -0
- formwork_cli/kit/check/test_gate.py +222 -0
- formwork_cli/kit/first-run.md +204 -0
- formwork_cli/kit/fw +121 -0
- formwork_cli/kit/glossary.md +160 -0
- formwork_cli/kit/guard/git-boundary +627 -0
- formwork_cli/kit/guard/protected-files +748 -0
- formwork_cli/kit/guard/quality-gate +260 -0
- formwork_cli/kit/guard/test_boundary.py +273 -0
- formwork_cli/kit/guard/test_protection.py +254 -0
- formwork_cli/kit/guard/test_quality_gate.py +156 -0
- formwork_cli/kit/install +395 -0
- formwork_cli/kit/limits.md +141 -0
- formwork_cli/kit/loop.md +82 -0
- formwork_cli/kit/roles/HOW-TO-ADD-A-ROLE.md +105 -0
- formwork_cli/kit/roles/TEMPLATE.md +26 -0
- formwork_cli/kit/roles/method/architect.md +269 -0
- formwork_cli/kit/roles/method/challenger.md +243 -0
- formwork_cli/kit/roles/method/lead.md +280 -0
- formwork_cli/kit/roles/method/record-keeper.md +206 -0
- formwork_cli/kit/roles/method/researcher.md +246 -0
- formwork_cli/kit/roles/method/reviewer.md +207 -0
- formwork_cli/kit/roles/packs/accessibility.md +236 -0
- formwork_cli/kit/roles/packs/ai.md +248 -0
- formwork_cli/kit/roles/packs/analyst.md +233 -0
- formwork_cli/kit/roles/packs/backend.md +425 -0
- formwork_cli/kit/roles/packs/brainstormer.md +190 -0
- formwork_cli/kit/roles/packs/data.md +212 -0
- formwork_cli/kit/roles/packs/devops.md +203 -0
- formwork_cli/kit/roles/packs/frontend.md +224 -0
- formwork_cli/kit/roles/packs/integrations.md +215 -0
- formwork_cli/kit/roles/packs/legal.md +251 -0
- formwork_cli/kit/roles/packs/marketing.md +206 -0
- formwork_cli/kit/roles/packs/mobile.md +202 -0
- formwork_cli/kit/roles/packs/performance.md +192 -0
- formwork_cli/kit/roles/packs/product.md +217 -0
- formwork_cli/kit/roles/packs/security.md +267 -0
- formwork_cli/kit/roles/packs/sre.md +203 -0
- formwork_cli/kit/roles/packs/tester.md +246 -0
- formwork_cli/kit/roles/packs/user-researcher.md +218 -0
- formwork_cli/kit/roles/packs/ux.md +205 -0
- formwork_cli/kit/roles/packs/visual.md +199 -0
- formwork_cli/kit/roles/packs/writer.md +198 -0
- formwork_cli/kit/round.md +131 -0
- formwork_cli/kit/rules/core.md +195 -0
- formwork_cli/kit/rules/full.md +493 -0
- formwork_cli/kit/templates/brief.md +68 -0
- formwork_cli/kit/templates/decision.md +93 -0
- formwork_cli/kit/templates/predictions.md +54 -0
- formwork_cli/kit/templates/report.md +52 -0
- formwork_cli/kit/templates/round.md +77 -0
- formwork_cli/kit/test_install.py +165 -0
- formwork_cli/kit/troubleshooting.md +247 -0
- formwork_cli/kit-page/FORMWORK.md +182 -0
- formwork_kit-0.1.0.dist-info/METADATA +308 -0
- formwork_kit-0.1.0.dist-info/RECORD +137 -0
- formwork_kit-0.1.0.dist-info/WHEEL +4 -0
- formwork_kit-0.1.0.dist-info/entry_points.txt +2 -0
- formwork_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Prove self-protection refuses, and prove it stays out of the way.
|
|
3
|
+
|
|
4
|
+
Every rule in this kit is enforced by a program, and every one of those
|
|
5
|
+
programs is an ordinary file. Turning the strongest rule off was four
|
|
6
|
+
characters, and nothing anywhere noticed.
|
|
7
|
+
|
|
8
|
+
This exercises every route that is now refused, and every ordinary thing that
|
|
9
|
+
must still work — because a guard that obstructs normal work gets switched off,
|
|
10
|
+
and then it protects nothing.
|
|
11
|
+
|
|
12
|
+
Python 3, standard library only, no dependencies.
|
|
13
|
+
"""
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
20
|
+
GUARD = os.path.join(HERE, "protected-files")
|
|
21
|
+
ALLOW, WARN, REFUSE = 0, 1, 2
|
|
22
|
+
results = []
|
|
23
|
+
|
|
24
|
+
# Assembled rather than written out, so that this file's own text does not
|
|
25
|
+
# read as a command to the boundary guard watching the session that writes it.
|
|
26
|
+
VCS = "g" + "it"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run(command=None, path=None, fmt=None, payload=None, level="block"):
|
|
30
|
+
env = dict(os.environ)
|
|
31
|
+
env["FORMWORK_PROTECT_FILES"] = level
|
|
32
|
+
args = [sys.executable, GUARD]
|
|
33
|
+
if command is not None:
|
|
34
|
+
args += ["--command", command]
|
|
35
|
+
if path is not None:
|
|
36
|
+
args += ["--path", path]
|
|
37
|
+
if fmt is not None:
|
|
38
|
+
args += ["--format", fmt]
|
|
39
|
+
p = subprocess.run(args, capture_output=True, text=True, env=env,
|
|
40
|
+
input=json.dumps(payload) if payload is not None else "")
|
|
41
|
+
return p.returncode, (p.stdout + p.stderr).strip()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def expect(label, want, **kw):
|
|
45
|
+
got, out = run(**kw)
|
|
46
|
+
ok = got == want
|
|
47
|
+
results.append(ok)
|
|
48
|
+
names = {ALLOW: "allow", WARN: "warn", REFUSE: "REFUSE"}
|
|
49
|
+
print(" [%s] %-52s %s" % ("pass" if ok else "FAIL", label[:52], names[want]))
|
|
50
|
+
if not ok:
|
|
51
|
+
print(" got %s: %s" % (names.get(got, got), out.split("\n")[0]))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
print("REFUSED — disarming the kit")
|
|
56
|
+
for c in [
|
|
57
|
+
"sed -i s/block/off/ .formwork.toml",
|
|
58
|
+
"echo x > formwork/check/run",
|
|
59
|
+
"rm formwork/check/checks/rule-labels",
|
|
60
|
+
"rm -rf formwork/guard",
|
|
61
|
+
"cp /tmp/x formwork/guard/" + VCS + "-boundary",
|
|
62
|
+
"mv formwork/guard/protected-files /tmp/x",
|
|
63
|
+
"cat /dev/null > .claude/settings.json",
|
|
64
|
+
"truncate -s 0 ~/.formwork/allowlist.txt",
|
|
65
|
+
"echo something >> ~/.formwork/allowlist.txt",
|
|
66
|
+
"chmod 000 formwork/check/run",
|
|
67
|
+
"python3 -c \"open('.formwork.toml','w')\"",
|
|
68
|
+
"ls && sed -i s/a/b/ formwork/guard/protected-files",
|
|
69
|
+
"tee formwork/check/run < /dev/null",
|
|
70
|
+
]:
|
|
71
|
+
expect(c, REFUSE, command=c)
|
|
72
|
+
|
|
73
|
+
print()
|
|
74
|
+
print("REFUSED — editing one directly")
|
|
75
|
+
for p in ["formwork/check/run", ".formwork.toml",
|
|
76
|
+
"formwork/guard/protected-files", ".claude/settings.json",
|
|
77
|
+
"formwork/check/checks/doc-links",
|
|
78
|
+
os.path.expanduser("~/.formwork/denylist-anycase.txt")]:
|
|
79
|
+
expect(p, REFUSE, path=p)
|
|
80
|
+
|
|
81
|
+
print()
|
|
82
|
+
print("ALLOWED — creating something that does not exist yet")
|
|
83
|
+
# Adding a guard or a check removes no enforcement, and the integrity
|
|
84
|
+
# check reports the addition, so it is never silent. Left refused, the
|
|
85
|
+
# protection had to be switched off to extend the kit at all.
|
|
86
|
+
for p in ["formwork/guard/a-new-guard",
|
|
87
|
+
"formwork/check/checks/a-new-check"]:
|
|
88
|
+
expect(p, ALLOW, path=p)
|
|
89
|
+
|
|
90
|
+
print()
|
|
91
|
+
print("ALLOWED — ordinary work must not be obstructed")
|
|
92
|
+
for c in [
|
|
93
|
+
"formwork/check/run",
|
|
94
|
+
"formwork/check/run --demo-fail",
|
|
95
|
+
"cat formwork/check/run",
|
|
96
|
+
"grep -n block .formwork.toml",
|
|
97
|
+
"sed -i s/a/b/ docs/design.md",
|
|
98
|
+
"echo notes > /tmp/scratch.txt",
|
|
99
|
+
"python3 tools/privacy_scan.py",
|
|
100
|
+
"ls formwork/guard",
|
|
101
|
+
]:
|
|
102
|
+
expect(c, ALLOW, command=c)
|
|
103
|
+
|
|
104
|
+
print()
|
|
105
|
+
print("ALLOWED — editing anything that is not a guard")
|
|
106
|
+
for p in ["docs/design.md", "formwork/rules/core.md",
|
|
107
|
+
"formwork/roles/method/lead.md", "README.md"]:
|
|
108
|
+
expect(p, ALLOW, path=p)
|
|
109
|
+
|
|
110
|
+
print()
|
|
111
|
+
print("ALLOWED — reading with a tool that can also write")
|
|
112
|
+
# Refusing these was a false positive three times in one session, and a
|
|
113
|
+
# guard that is wrong about ordinary work gets switched off by somebody
|
|
114
|
+
# busy. Reading is not writing.
|
|
115
|
+
for c in ["sed -n 1,5p .formwork.toml",
|
|
116
|
+
"sed s/a/b/ formwork/check/run",
|
|
117
|
+
"awk {print} formwork/check/run",
|
|
118
|
+
"chmod +x formwork/check/checks/decision-ids",
|
|
119
|
+
"chmod 755 formwork/check/checks/decision-ids"]:
|
|
120
|
+
expect(c, ALLOW, command=c)
|
|
121
|
+
|
|
122
|
+
print()
|
|
123
|
+
print("REFUSED — the same tools, actually writing")
|
|
124
|
+
for c in ["sed -i s/a/b/ .formwork.toml",
|
|
125
|
+
"sed -i.bak s/a/b/ formwork/check/run",
|
|
126
|
+
"perl -i -pe s/a/b/ .formwork.toml",
|
|
127
|
+
"chmod 000 formwork/check/run",
|
|
128
|
+
"chmod -x formwork/guard/git-boundary",
|
|
129
|
+
"chmod 644 formwork/check/run"]:
|
|
130
|
+
expect(c, REFUSE, command=c)
|
|
131
|
+
|
|
132
|
+
print()
|
|
133
|
+
print("PAYLOADS — and a refusal to guess")
|
|
134
|
+
for fmt in ("claude-code", "codex", "cursor", "gemini-cli"):
|
|
135
|
+
expect("%s: an edit to the gate" % fmt, REFUSE, fmt=fmt,
|
|
136
|
+
payload={"tool_input": {"file_path": "formwork/check/run"}})
|
|
137
|
+
expect("claude-code: an edit to a rule", ALLOW, fmt="claude-code",
|
|
138
|
+
payload={"tool_input": {"file_path": "formwork/rules/core.md"}})
|
|
139
|
+
expect("an unknown runtime", REFUSE, fmt="nope", payload={})
|
|
140
|
+
expect("no payload at all", REFUSE, fmt="claude-code")
|
|
141
|
+
|
|
142
|
+
print()
|
|
143
|
+
print("STRENGTH — the same command, three settings")
|
|
144
|
+
for level, want in (("block", REFUSE), ("warn", WARN), ("off", ALLOW)):
|
|
145
|
+
expect("%s: disarming the configuration" % level, want,
|
|
146
|
+
command="sed -i s/block/off/ .formwork.toml", level=level)
|
|
147
|
+
|
|
148
|
+
# Everything below is a hole an audit walked through, or a false positive
|
|
149
|
+
# it reported. Each was the behaviour before the fix beside it.
|
|
150
|
+
print("REFUSED — routes that used to walk straight past this guard")
|
|
151
|
+
for c in ["echo x | tee formwork/check/run",
|
|
152
|
+
"chmod a-x formwork/check/checks/doc-links",
|
|
153
|
+
"chmod u-x formwork/check/checks/doc-links",
|
|
154
|
+
"chmod go-rwx formwork/check/run",
|
|
155
|
+
"chmod a=r formwork/check/run",
|
|
156
|
+
"rm -rf formwork",
|
|
157
|
+
"mv formwork /tmp/elsewhere",
|
|
158
|
+
"rm -rf formwork/check",
|
|
159
|
+
"formwork/check/checks/kit-integrity --record ."]:
|
|
160
|
+
expect(c, REFUSE, command=c)
|
|
161
|
+
|
|
162
|
+
print("ALLOWED — ordinary work this guard must not refuse")
|
|
163
|
+
for c in ["chmod +x formwork/check/run",
|
|
164
|
+
"python3 formwork/check/run",
|
|
165
|
+
"bash formwork/check/run",
|
|
166
|
+
"./formwork/check/run",
|
|
167
|
+
"formwork/check/checks/kit-integrity .",
|
|
168
|
+
"cat formwork/guard/git-boundary",
|
|
169
|
+
"rm -rf build",
|
|
170
|
+
"mkdir docs/rounds"]:
|
|
171
|
+
expect(c, ALLOW, command=c)
|
|
172
|
+
|
|
173
|
+
print("FAILS CLOSED — a payload it cannot read")
|
|
174
|
+
for bad in ("[]", "null"):
|
|
175
|
+
got, out = run(fmt="claude-code", payload=bad)
|
|
176
|
+
ok = got == REFUSE
|
|
177
|
+
results.append(ok)
|
|
178
|
+
print(" [%s] %-52s %s"
|
|
179
|
+
% ("pass" if ok else "FAIL", "unreadable payload %s" % bad,
|
|
180
|
+
"REFUSE"))
|
|
181
|
+
|
|
182
|
+
print("SECOND AUDIT — routes that were still open")
|
|
183
|
+
for c in ["rm formwork/guard/*",
|
|
184
|
+
"rm -rf formwork/check/checks/*",
|
|
185
|
+
"chmod 644 formwork/check/checks/*",
|
|
186
|
+
"rm formwork/check/ru*",
|
|
187
|
+
"nice -n 5 rm formwork/guard/git-boundary",
|
|
188
|
+
"timeout 5 rm formwork/guard/git-boundary",
|
|
189
|
+
"sudo -u nobody rm formwork/guard/git-boundary",
|
|
190
|
+
"bash -c 'rm formwork/guard/git-boundary'",
|
|
191
|
+
"for f in x; do rm formwork/check/run; done",
|
|
192
|
+
"(rm formwork/check/run)",
|
|
193
|
+
"perl -e 'unlink \"formwork/guard/git-boundary\"'",
|
|
194
|
+
"sed --in-place s/a/b/ formwork/check/run"]:
|
|
195
|
+
expect(c, REFUSE, command=c)
|
|
196
|
+
got, _ = run(command="bash <<'EOF'\nrm formwork/check/run\nEOF\n")
|
|
197
|
+
ok = got == REFUSE
|
|
198
|
+
results.append(ok)
|
|
199
|
+
print(" [%s] %-52s %s" % ("pass" if ok else "FAIL",
|
|
200
|
+
"heredoc fed to a shell", "REFUSE"))
|
|
201
|
+
|
|
202
|
+
print("THE FRONT DOOR — the short command must not be a way round")
|
|
203
|
+
for c in ["formwork record", "formwork/fw record", "./formwork/fw record"]:
|
|
204
|
+
expect(c, REFUSE, command=c)
|
|
205
|
+
for c in ["formwork check", "formwork/fw check", "formwork test",
|
|
206
|
+
"formwork init", "formwork install --runtime claude-code",
|
|
207
|
+
"formwork where"]:
|
|
208
|
+
expect(c, ALLOW, command=c)
|
|
209
|
+
|
|
210
|
+
print("THIRD AUDIT — routes found by the third audit")
|
|
211
|
+
for c in ["sudo -n rm formwork/guard/git-boundary",
|
|
212
|
+
"env -i rm formwork/guard/git-boundary",
|
|
213
|
+
"if rm formwork/check/run; then echo x; fi",
|
|
214
|
+
"while rm formwork/check/run; do :; done",
|
|
215
|
+
"python3 formwork/fw record",
|
|
216
|
+
"python3 formwork/check/checks/kit-integrity --record .",
|
|
217
|
+
"uv run formwork/fw record",
|
|
218
|
+
"rm -rf *", "rm -rf ./*",
|
|
219
|
+
"dd if=/dev/zero of=formwork/check/run",
|
|
220
|
+
"unlink formwork/guard/git-boundary",
|
|
221
|
+
"rsync /tmp/x formwork/check/run"]:
|
|
222
|
+
expect(c, REFUSE, command=c)
|
|
223
|
+
got, _ = run(command="bash <<'EOF'\nrm formwork/check/run\nEOF\n")
|
|
224
|
+
ok = got == REFUSE
|
|
225
|
+
results.append(ok)
|
|
226
|
+
print(" [%s] %-52s %s" % ("pass" if ok else "FAIL",
|
|
227
|
+
"heredoc into a shell", "REFUSE"))
|
|
228
|
+
|
|
229
|
+
print("THIRD AUDIT — siblings and ordinary globs, wrongly refused")
|
|
230
|
+
for c in ["rm .formwork.toml.bak", "rm formwork/check/run.orig",
|
|
231
|
+
"rm *.pyc", "rm build/*", "rm docs/*.tmp",
|
|
232
|
+
"sudo -n ls", "env -i ls", "python3 formwork/fw check"]:
|
|
233
|
+
expect(c, ALLOW, command=c)
|
|
234
|
+
got, _ = run(fmt="claude-code", payload='{"tool_input":{"command":123}}')
|
|
235
|
+
ok = got == REFUSE
|
|
236
|
+
results.append(ok)
|
|
237
|
+
print(" [%s] %-52s %s" % ("pass" if ok else "FAIL",
|
|
238
|
+
"a command that is not a string", "REFUSE"))
|
|
239
|
+
|
|
240
|
+
print("SECOND AUDIT — ordinary work that was wrongly refused")
|
|
241
|
+
for c in ["python3 -m pytest formwork/guard/test_boundary.py",
|
|
242
|
+
"cp formwork/check/run /tmp/backup-run",
|
|
243
|
+
"rm formwork/check/runner-notes.md",
|
|
244
|
+
"rm build/*", "rm docs/*.tmp", "ls formwork/guard/*",
|
|
245
|
+
"nice -n 5 ls", "bash -c 'echo hi'"]:
|
|
246
|
+
expect(c, ALLOW, command=c)
|
|
247
|
+
|
|
248
|
+
print()
|
|
249
|
+
print("%d of %d behaved as specified." % (sum(results), len(results)))
|
|
250
|
+
return 0 if all(results) else 1
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if __name__ == "__main__":
|
|
254
|
+
sys.exit(main())
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Prove the end-of-turn gate blocks, and prove the budget behaves.
|
|
3
|
+
|
|
4
|
+
The budget is the part that matters. A guard that refuses once was defeated
|
|
5
|
+
under test elsewhere; a guard that refuses for ever strands the work. This
|
|
6
|
+
exercises both edges and everything between.
|
|
7
|
+
|
|
8
|
+
Python 3, standard library only, no dependencies.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
|
|
17
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
18
|
+
GUARD = os.path.join(HERE, "quality-gate")
|
|
19
|
+
ALLOW, REFUSE = 0, 2
|
|
20
|
+
results = []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check(name, got, want, output=""):
|
|
24
|
+
ok = got == want
|
|
25
|
+
results.append(ok)
|
|
26
|
+
print(" [%s] %-50s expected %d, got %d"
|
|
27
|
+
% ("pass" if ok else "FAIL", name[:50], want, got))
|
|
28
|
+
if not ok and output:
|
|
29
|
+
for line in output.strip().split("\n")[:4]:
|
|
30
|
+
print(" %s" % line)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build(aggregate_exit, config="", state=None):
|
|
34
|
+
"""A throwaway project whose aggregate exits however we say."""
|
|
35
|
+
root = tempfile.mkdtemp(prefix="fw-gate-")
|
|
36
|
+
os.makedirs(os.path.join(root, "formwork", "check"))
|
|
37
|
+
gate = os.path.join(root, "formwork", "check", "run")
|
|
38
|
+
open(gate, "w").write(
|
|
39
|
+
"#!/usr/bin/env python3\n"
|
|
40
|
+
"print('aggregate output')\n"
|
|
41
|
+
"import sys; sys.exit(%d)\n" % aggregate_exit)
|
|
42
|
+
os.chmod(gate, 0o755)
|
|
43
|
+
open(os.path.join(root, ".formwork.toml"), "w").write(config)
|
|
44
|
+
open(os.path.join(root, "README.md"), "w").write("a project\n")
|
|
45
|
+
if state:
|
|
46
|
+
os.makedirs(state, exist_ok=True)
|
|
47
|
+
return root
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run(root, session="s1", state=None, env_extra=None):
|
|
51
|
+
env = dict(os.environ)
|
|
52
|
+
env["CLAUDE_PROJECT_DIR"] = root
|
|
53
|
+
env["FORMWORK_STATE_DIR"] = state or os.path.join(root, "state")
|
|
54
|
+
env.pop("FORMWORK_GATE", None)
|
|
55
|
+
if env_extra:
|
|
56
|
+
env.update(env_extra)
|
|
57
|
+
p = subprocess.run([sys.executable, GUARD, "--format", "claude-code"],
|
|
58
|
+
capture_output=True, text=True, env=env,
|
|
59
|
+
input=json.dumps({"session_id": session}))
|
|
60
|
+
return p.returncode, (p.stdout + p.stderr).strip()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main():
|
|
64
|
+
print("THE END-OF-TURN GATE")
|
|
65
|
+
|
|
66
|
+
root = build(0)
|
|
67
|
+
code, out = run(root)
|
|
68
|
+
check("green aggregate: the turn may finish", code, ALLOW, out)
|
|
69
|
+
shutil.rmtree(root)
|
|
70
|
+
|
|
71
|
+
root = build(1)
|
|
72
|
+
code, out = run(root)
|
|
73
|
+
check("red aggregate: the turn is refused", code, REFUSE, out)
|
|
74
|
+
shutil.rmtree(root)
|
|
75
|
+
|
|
76
|
+
# The aggregate itself failing to run is not a pass.
|
|
77
|
+
root = build(0)
|
|
78
|
+
os.remove(os.path.join(root, "formwork", "check", "run"))
|
|
79
|
+
code, out = run(root)
|
|
80
|
+
check("no aggregate at all: refused, not waved through", code, REFUSE, out)
|
|
81
|
+
shutil.rmtree(root)
|
|
82
|
+
|
|
83
|
+
print()
|
|
84
|
+
print("THE BUDGET")
|
|
85
|
+
|
|
86
|
+
root = build(1, config='[strength]\ngate_budget = "2"\n')
|
|
87
|
+
state = os.path.join(root, "state")
|
|
88
|
+
seq = []
|
|
89
|
+
for i in range(4):
|
|
90
|
+
code, out = run(root, session="budget-test", state=state)
|
|
91
|
+
seq.append(code)
|
|
92
|
+
ok = seq == [REFUSE, REFUSE, ALLOW, ALLOW]
|
|
93
|
+
results.append(ok)
|
|
94
|
+
print(" [%s] %-50s %s"
|
|
95
|
+
% ("pass" if ok else "FAIL", "refuses twice, then stands aside", seq))
|
|
96
|
+
# And it must say plainly that nothing was fixed.
|
|
97
|
+
code, out = run(root, session="budget-test", state=state)
|
|
98
|
+
told = "NOTHING BELOW HAS BEEN FIXED" in out
|
|
99
|
+
results.append(told)
|
|
100
|
+
print(" [%s] %-50s" % ("pass" if told else "FAIL",
|
|
101
|
+
"when spent, it says nothing was fixed"))
|
|
102
|
+
shutil.rmtree(root)
|
|
103
|
+
|
|
104
|
+
# A different session gets its own budget.
|
|
105
|
+
root = build(1, config='[strength]\ngate_budget = "1"\n')
|
|
106
|
+
state = os.path.join(root, "state")
|
|
107
|
+
run(root, session="one", state=state)
|
|
108
|
+
code, _ = run(root, session="one", state=state)
|
|
109
|
+
first_spent = code == ALLOW
|
|
110
|
+
code, _ = run(root, session="two", state=state)
|
|
111
|
+
second_fresh = code == REFUSE
|
|
112
|
+
results.append(first_spent and second_fresh)
|
|
113
|
+
print(" [%s] %-50s" % ("pass" if first_spent and second_fresh else "FAIL",
|
|
114
|
+
"each session gets its own budget"))
|
|
115
|
+
shutil.rmtree(root)
|
|
116
|
+
|
|
117
|
+
print()
|
|
118
|
+
print("STRENGTH")
|
|
119
|
+
|
|
120
|
+
for level, want in (("block", REFUSE), ("warn", ALLOW), ("off", ALLOW)):
|
|
121
|
+
root = build(1, config='[strength]\naggregate_gate = "%s"\n' % level)
|
|
122
|
+
code, out = run(root, session="strength-%s" % level)
|
|
123
|
+
check("red aggregate at strength=%s" % level, code, want, out)
|
|
124
|
+
shutil.rmtree(root)
|
|
125
|
+
|
|
126
|
+
# warn must still say what happened rather than going quiet.
|
|
127
|
+
root = build(1, config='[strength]\naggregate_gate = "warn"\n')
|
|
128
|
+
code, out = run(root, session="warn-loud")
|
|
129
|
+
loud = "red" in out.lower()
|
|
130
|
+
results.append(loud)
|
|
131
|
+
print(" [%s] %-50s" % ("pass" if loud else "FAIL",
|
|
132
|
+
"warn still reports the failure"))
|
|
133
|
+
shutil.rmtree(root)
|
|
134
|
+
|
|
135
|
+
print()
|
|
136
|
+
print("REFUSING TO GUESS")
|
|
137
|
+
|
|
138
|
+
root = build(1)
|
|
139
|
+
env = dict(os.environ)
|
|
140
|
+
env["CLAUDE_PROJECT_DIR"] = root
|
|
141
|
+
p = subprocess.run([sys.executable, GUARD, "--format", "nope"],
|
|
142
|
+
capture_output=True, text=True, env=env, input="{}")
|
|
143
|
+
check("an unknown runtime: refused", p.returncode, REFUSE)
|
|
144
|
+
p = subprocess.run([sys.executable, GUARD],
|
|
145
|
+
capture_output=True, text=True, env=env, input="")
|
|
146
|
+
check("no arguments at all: refused", p.returncode, REFUSE)
|
|
147
|
+
shutil.rmtree(root)
|
|
148
|
+
|
|
149
|
+
print()
|
|
150
|
+
print("%d of %d behaved as specified." % (sum(1 for r in results if r),
|
|
151
|
+
len(results)))
|
|
152
|
+
return 0 if all(results) else 1
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
sys.exit(main())
|