test_upgrade_regime_security.py python
220 lines 6.3 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """Security — Track O / O3 upgrade-regime fail-closed (§O2.9 security)."""
2
3 from __future__ import annotations
4
5 import json
6 import sys
7 from io import StringIO
8 from pathlib import Path
9
10 from adapters.templating import render_template
11 from adapters.config import load_config
12 from cli.context import CliContext
13 from cli.kit_root import kit_root
14 from cli.main import main
15 from cli.output import OutputContext
16 from tests.support import (
17 FIXTURES,
18 make_runner,
19 muse_mirror_status_runner,
20 muse_status_runner,
21 ok,
22 run_cli,
23 seed_muse_substrate,
24 )
25 from tools.upgrade_regime.ceremony import (
26 check_g3_deploy_script,
27 check_g4_deploy_script,
28 )
29
30
31 def test_no_secrets_in_ceremony_json(tmp_path: Path) -> None:
32 seed_muse_substrate(tmp_path)
33 assert (
34 run_cli(
35 [
36 "init",
37 "--from-config",
38 str(FIXTURES / "config-muse-only.yaml"),
39 "--non-interactive",
40 ],
41 cwd=tmp_path,
42 kit=kit_root(),
43 runner=muse_status_runner(tmp_path),
44 )
45 == 0
46 )
47 base = muse_mirror_status_runner(tmp_path)
48 responses = dict(base.responses)
49 responses["git remote get-url origin"] = ok("[email protected]:o/r.git")
50 buf = StringIO()
51 old = sys.stdout
52 sys.stdout = buf
53 try:
54 ctx = CliContext.create(
55 runner=make_runner(responses),
56 cwd=tmp_path,
57 kit=kit_root(),
58 output=OutputContext(json_mode=True),
59 )
60 code = main(
61 [
62 "upgrade-regime",
63 "--from",
64 "muse-only",
65 "--to",
66 "muse+git-mirror",
67 "--dry-run",
68 "--json",
69 ],
70 ctx=ctx,
71 )
72 finally:
73 sys.stdout = old
74 assert code == 0
75 out = buf.getvalue()
76 assert "AKIA" not in out
77 assert "BEGIN PRIVATE KEY" not in out
78 assert "/Users/" not in out
79 payload = json.loads(out)
80 assert "gates" in payload
81
82
83 def test_g3_g4_refuse_git_dir_dot_and_push_main() -> None:
84 config = load_config(FIXTURES / "config-muse-git-mirror.yaml")
85 script = render_template(
86 kit_root() / "templates" / "scripts" / "muse-bridge-deploy.sh.template",
87 config,
88 )
89 assert check_g3_deploy_script(script).ok
90 assert check_g4_deploy_script(script).ok
91 bad = '#!/bin/bash\nmuse bridge git-export --git-dir .\ngit push origin main\n'
92 assert not check_g3_deploy_script(bad).ok
93 assert not check_g4_deploy_script(bad).ok
94
95
96 def test_path_escape_config_refused(tmp_path: Path) -> None:
97 seed_muse_substrate(tmp_path)
98 outside = tmp_path / ".." / "outside-config.yaml"
99 # Prefer absolute path outside repo
100 outside = tmp_path.parent / f"escape-config-{tmp_path.name}.yaml"
101 outside.write_text("overseer_config_version: 1\n", encoding="utf-8")
102 try:
103 code = run_cli(
104 [
105 "--config",
106 str(outside),
107 "upgrade-regime",
108 "--from",
109 "muse-only",
110 "--to",
111 "muse+git-mirror",
112 "--dry-run",
113 ],
114 cwd=tmp_path,
115 kit=kit_root(),
116 )
117 assert code == 4
118 finally:
119 outside.unlink(missing_ok=True)
120
121
122 def test_live_bridge_without_yes_refused(tmp_path: Path) -> None:
123 seed_muse_substrate(tmp_path)
124 assert (
125 run_cli(
126 [
127 "init",
128 "--from-config",
129 str(FIXTURES / "config-muse-only.yaml"),
130 "--non-interactive",
131 ],
132 cwd=tmp_path,
133 kit=kit_root(),
134 runner=muse_status_runner(tmp_path),
135 )
136 == 0
137 )
138 base = muse_mirror_status_runner(tmp_path)
139 responses = dict(base.responses)
140 responses["git remote get-url origin"] = ok("[email protected]:o/r.git")
141 # Apply first so gates can pass, then live without -y
142 assert (
143 run_cli(
144 ["upgrade-regime", "--from", "muse-only", "--to", "muse+git-mirror", "--apply"],
145 cwd=tmp_path,
146 kit=kit_root(),
147 runner=make_runner(responses),
148 )
149 == 0
150 )
151 code = run_cli(
152 [
153 "upgrade-regime",
154 "--from",
155 "muse-only",
156 "--to",
157 "muse+git-mirror",
158 "--apply",
159 "--live-bridge",
160 ],
161 cwd=tmp_path,
162 kit=kit_root(),
163 runner=make_runner(responses),
164 )
165 assert code == 4
166
167
168 def test_git_only_baseline_unchanged_by_ceremony(tmp_path: Path) -> None:
169 """K7 MuseHub-optional: git-only trees are refused, not rewritten."""
170 assert (
171 run_cli(
172 ["init", "--regime", "git-only", "--non-interactive"],
173 cwd=tmp_path,
174 kit=kit_root(),
175 )
176 == 0
177 )
178 before = (tmp_path / ".overseer" / "config.yaml").read_bytes()
179 code = run_cli(
180 ["upgrade-regime", "--from", "muse-only", "--to", "muse+git-mirror", "--apply"],
181 cwd=tmp_path,
182 kit=kit_root(),
183 )
184 assert code == 4
185 assert (tmp_path / ".overseer" / "config.yaml").read_bytes() == before
186
187
188 def test_no_network_required_for_c0_c5(tmp_path: Path) -> None:
189 """RecordingRunner only — no SubprocessRunner network for dry-run path."""
190 seed_muse_substrate(tmp_path)
191 assert (
192 run_cli(
193 [
194 "init",
195 "--from-config",
196 str(FIXTURES / "config-muse-only.yaml"),
197 "--non-interactive",
198 ],
199 cwd=tmp_path,
200 kit=kit_root(),
201 runner=muse_status_runner(tmp_path),
202 )
203 == 0
204 )
205 base = muse_mirror_status_runner(tmp_path)
206 responses = dict(base.responses)
207 responses["git remote get-url origin"] = ok("[email protected]:o/r.git")
208 runner = make_runner(responses)
209 code = run_cli(
210 ["upgrade-regime", "--from", "muse-only", "--to", "muse+git-mirror", "--dry-run"],
211 cwd=tmp_path,
212 kit=kit_root(),
213 runner=runner,
214 )
215 assert code == 0
216 # Only local git remote get-url (mocked) — no curl/gh/muse export
217 for cmd, _cwd in runner.calls:
218 assert "curl" not in cmd
219 assert "gh " not in cmd
220 assert "bridge git-export" not in cmd
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago