@heretek-ai/epistemic-swarm 0.7.21 → 0.7.23
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/bin/cli.js +3 -2
- package/extensions/pi/index.js +3 -2
- package/package.json +5 -1
- package/plugins/antigravity/plugin.json +1 -1
- package/plugins/factory/skills/factory/SKILL.md +1 -1
- package/plugins/opencode/analysis-bridge.js +566 -0
- package/plugins/opencode/config-io.js +598 -31
- package/plugins/opencode/dep-health.js +1039 -0
- package/plugins/opencode/hook-bus.js +434 -0
- package/plugins/opencode/index.js +252 -19
- package/plugins/opencode/tui.js +230 -17
- package/plugins/opencode/weight-baseline.json +16 -0
- package/plugins/opencode/weight-signals.js +769 -0
- package/plugins/research-cache/skills/research_cache/hasher.py +33 -8
- package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
- package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
- package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
- package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
- package/runner/__pycache__/errors.cpython-311.pyc +0 -0
- package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
- package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
- package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
- package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
- package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
- package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
- package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
- package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/mcp_server.py +249 -81
- package/runner/refinement.py +5 -0
- package/runner/schema_validate.py +63 -1
- package/runner/schemas.py +9 -4
- package/runner/tests/__pycache__/test_analysis_bridge.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_antigravity_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_config_schema.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dep_health.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hardening_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hook_bus.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_phase08_waiver_reopens.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_weight_signals.cpython-311.pyc +0 -0
- package/runner/tests/fixtures/config_validation_cases.json +60 -0
- package/runner/tests/test_analysis_bridge.py +356 -0
- package/runner/tests/test_config_schema.py +119 -15
- package/runner/tests/test_dep_health.py +484 -0
- package/runner/tests/test_hardening_contract.py +391 -0
- package/runner/tests/test_hook_bus.py +802 -0
- package/runner/tests/test_opencode_ux.py +232 -0
- package/runner/tests/test_opencode_v2_registrars.py +548 -16
- package/runner/tests/test_phase08_waiver_reopens.py +539 -0
- package/runner/tests/test_weight_signals.py +361 -0
- package/schemas/config.schema.json +4 -3
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
- package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/factory/SKILL.md +1 -1
- package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/research_cache/hasher.py +33 -8
- package/skills/swarm_config/SKILL.md +6 -1
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
- package/skills/swarm_config/configure.py +254 -17
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase 08 waiver re-opens (W9, W17, W18, W6) — R1-R4 pinning tests.
|
|
3
|
+
|
|
4
|
+
Dossier evidence hashes (`.roadmap/08-waiver-reopens/dossier.json`):
|
|
5
|
+
- index.js sha256:3a4a4cb6ce2bc34b72246291b321009a5a09c67c393467fd07d5cc0e68d9dac3
|
|
6
|
+
- configure.py sha256:10c893d88e9953174944fb04a3d49a7c9b63655a6da671d8eab0ff24b42a3425
|
|
7
|
+
- preflight.py sha256:762d31ced84b10e86203d88f104704c9763f23838612fba02652018a0c80797e
|
|
8
|
+
- mcp_server.py sha256:34276d2db3035f75dda74cf0b64cabf4325c423686de14325aec808e6da8e20f
|
|
9
|
+
|
|
10
|
+
R1 (W9): sanitizer single-segment/file:// boundary refined to
|
|
11
|
+
system-internal disclosure, per-item, without over-redacting user echoes.
|
|
12
|
+
R2 (W17): FIFO-named-config hang hardened — bounded-reader port to PY
|
|
13
|
+
`configure.py` and JS `config-io.js`; the hang-regression proof runs the
|
|
14
|
+
FIFO-touching reads in a subprocess with a timeout (pre-fix they block
|
|
15
|
+
forever), while in-process tests pin normal-file equivalence.
|
|
16
|
+
R3 (W18): `heal_config` zero-caller docstrings corrected to the true
|
|
17
|
+
non-healing-read architecture (+ SKILL.md).
|
|
18
|
+
R4 (W6): `gate` reachable through the `iumbtems_factory` tool surface
|
|
19
|
+
(PY handler forwards --run/--phase/--action/--reason; JS catalog enum).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
import unittest
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
32
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
33
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_node(code, timeout=60):
|
|
37
|
+
return subprocess.run(
|
|
38
|
+
["node", "--input-type=module", "-e", code],
|
|
39
|
+
capture_output=True,
|
|
40
|
+
text=True,
|
|
41
|
+
cwd=str(PROJECT_ROOT),
|
|
42
|
+
timeout=timeout,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def last_json_object(stdout):
|
|
47
|
+
lines = [l.strip() for l in stdout.strip().split("\n") if l.strip().startswith("{")]
|
|
48
|
+
return json.loads(lines[-1])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
SETTINGS_PROLOGUE = """
|
|
52
|
+
import { createSettingsHandlers } from "./plugins/opencode/index.js";
|
|
53
|
+
function rpcCtx() {
|
|
54
|
+
return {
|
|
55
|
+
signal: null,
|
|
56
|
+
error: (type, message, data) => {
|
|
57
|
+
const e = new Error(message);
|
|
58
|
+
e.type = type;
|
|
59
|
+
e.data = data;
|
|
60
|
+
throw e;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const FIXED = "Config write rejected: canonical config surface returned an unreadable failure.";
|
|
65
|
+
async function probe(items) {
|
|
66
|
+
const handlers = createSettingsHandlers({
|
|
67
|
+
root: "/tmp/oc-p08",
|
|
68
|
+
callMcpImpl: async () => ({
|
|
69
|
+
content: JSON.stringify({ status: "error", code: "CONFIG_VALIDATION_FAILED", written: false, errors: items }),
|
|
70
|
+
status: "success",
|
|
71
|
+
}),
|
|
72
|
+
storage: null,
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
await handlers.set({ updates: { mode: "scout" } }, rpcCtx());
|
|
76
|
+
return { threw: false };
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return { threw: true, type: e.type || e.rpcType || null, errors: (e.data || e.rpcData || {}).errors || null };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestR1SanitizerBoundary(unittest.TestCase):
|
|
85
|
+
"""R1 (W9): single-segment system roots + file:// URIs are system-internal
|
|
86
|
+
disclosures (redacted per-item); ordinary user echoes still pass."""
|
|
87
|
+
|
|
88
|
+
CLEAN_ENUM = "mode: value 'nope' not in enum ['research', 'audit']"
|
|
89
|
+
# A user-echoed single-segment value that is NOT a system root.
|
|
90
|
+
CLEAN_SINGLE_SEGMENT_ECHO = "mode: value '/v1' not in enum ['research', 'audit']"
|
|
91
|
+
|
|
92
|
+
def _probe(self, items):
|
|
93
|
+
res = run_node(
|
|
94
|
+
SETTINGS_PROLOGUE
|
|
95
|
+
+ f"""
|
|
96
|
+
const out = await probe({json.dumps(items)});
|
|
97
|
+
console.log(JSON.stringify(out));
|
|
98
|
+
"""
|
|
99
|
+
)
|
|
100
|
+
self.assertEqual(res.returncode, 0, res.stderr)
|
|
101
|
+
return last_json_object(res.stdout)
|
|
102
|
+
|
|
103
|
+
def test_single_segment_system_roots_redacted(self):
|
|
104
|
+
for item in (
|
|
105
|
+
"python3: can't open file '/etc/iumbtems.json': [Errno 2] No such file or directory",
|
|
106
|
+
"config path /tmp/pipe is not a regular file",
|
|
107
|
+
"failed to read /var state",
|
|
108
|
+
):
|
|
109
|
+
with self.subTest(item=item):
|
|
110
|
+
d = self._probe([item])
|
|
111
|
+
self.assertTrue(d["threw"])
|
|
112
|
+
self.assertEqual(
|
|
113
|
+
d["errors"],
|
|
114
|
+
[
|
|
115
|
+
"Config write rejected: canonical config surface returned an "
|
|
116
|
+
"unreadable failure."
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def test_file_uris_redacted(self):
|
|
121
|
+
for item in (
|
|
122
|
+
"python3: can't open 'file:///etc/iumbtems/config.json': [Errno 2]",
|
|
123
|
+
"fetch failed for file://server/share/config.json",
|
|
124
|
+
):
|
|
125
|
+
with self.subTest(item=item):
|
|
126
|
+
d = self._probe([item])
|
|
127
|
+
self.assertTrue(d["threw"])
|
|
128
|
+
self.assertEqual(len(d["errors"]), 1)
|
|
129
|
+
self.assertTrue(d["errors"][0].startswith("Config write rejected:"))
|
|
130
|
+
|
|
131
|
+
def test_user_echoes_still_pass(self):
|
|
132
|
+
for item in (self.CLEAN_ENUM, self.CLEAN_SINGLE_SEGMENT_ECHO):
|
|
133
|
+
with self.subTest(item=item):
|
|
134
|
+
d = self._probe([item])
|
|
135
|
+
self.assertTrue(d["threw"])
|
|
136
|
+
self.assertEqual(d["errors"], [item])
|
|
137
|
+
|
|
138
|
+
def test_per_item_design_holds(self):
|
|
139
|
+
d = self._probe([self.CLEAN_ENUM, "/etc/iumbtems.json is missing"])
|
|
140
|
+
self.assertTrue(d["threw"])
|
|
141
|
+
self.assertEqual(len(d["errors"]), 2)
|
|
142
|
+
self.assertEqual(d["errors"][0], self.CLEAN_ENUM)
|
|
143
|
+
self.assertTrue(d["errors"][1].startswith("Config write rejected:"))
|
|
144
|
+
|
|
145
|
+
def test_bare_root_trailing_punct_redacted(self):
|
|
146
|
+
# REWORK-2 L1: bare-root + sentence punctuation redacts; multi-segment
|
|
147
|
+
# `/etc/foo.` already redacted via the >=2-segment arm (untouched).
|
|
148
|
+
for item in (
|
|
149
|
+
"/lib.",
|
|
150
|
+
"/etc.",
|
|
151
|
+
"/lib!",
|
|
152
|
+
"/tmp?",
|
|
153
|
+
"/lib]",
|
|
154
|
+
"/lib}",
|
|
155
|
+
"error at /lib.",
|
|
156
|
+
"failed on /etc!",
|
|
157
|
+
"saw /tmp?",
|
|
158
|
+
"x /lib] y",
|
|
159
|
+
"x /lib} y",
|
|
160
|
+
):
|
|
161
|
+
with self.subTest(item=item):
|
|
162
|
+
d = self._probe([item])
|
|
163
|
+
self.assertTrue(d["threw"])
|
|
164
|
+
self.assertEqual(len(d["errors"]), 1)
|
|
165
|
+
self.assertTrue(d["errors"][0].startswith("Config write rejected:"))
|
|
166
|
+
|
|
167
|
+
def test_bracket_brace_dash_prefix_redacted(self):
|
|
168
|
+
# REWORK-2 L2: `[`, `{`, `-` prefix members redact.
|
|
169
|
+
for item in (
|
|
170
|
+
"[/etc]",
|
|
171
|
+
"[/lib]",
|
|
172
|
+
"[/lib64]",
|
|
173
|
+
"{/etc}",
|
|
174
|
+
"-/etc",
|
|
175
|
+
"x-/lib64,y",
|
|
176
|
+
"error [/lib] here",
|
|
177
|
+
"err {/etc} here",
|
|
178
|
+
):
|
|
179
|
+
with self.subTest(item=item):
|
|
180
|
+
d = self._probe([item])
|
|
181
|
+
self.assertTrue(d["threw"])
|
|
182
|
+
self.assertEqual(len(d["errors"]), 1)
|
|
183
|
+
self.assertTrue(d["errors"][0].startswith("Config write rejected:"))
|
|
184
|
+
|
|
185
|
+
def test_canonical_corpus_passes_verbatim(self):
|
|
186
|
+
# Over-redaction gate (blocking): the full canonical-value corpus must
|
|
187
|
+
# ALL pass verbatim — this is the `-`-prefix safety proof (canonical
|
|
188
|
+
# values contain no `/`-root tokens, so `-`-prefixed path tokens are
|
|
189
|
+
# disclosures). If any of these redacted, the `-` member must be
|
|
190
|
+
# dropped and re-waived with trigger instead of forcing it.
|
|
191
|
+
corpus = (
|
|
192
|
+
"duckduckgo",
|
|
193
|
+
"brave",
|
|
194
|
+
"firecrawl",
|
|
195
|
+
"searxng",
|
|
196
|
+
"research",
|
|
197
|
+
"audit",
|
|
198
|
+
"scout",
|
|
199
|
+
"hybrid",
|
|
200
|
+
"brainstorm",
|
|
201
|
+
"darkharvest",
|
|
202
|
+
"auto",
|
|
203
|
+
"claude",
|
|
204
|
+
"opencode",
|
|
205
|
+
"MIT",
|
|
206
|
+
"Apache-2.0",
|
|
207
|
+
"BSD-3-Clause",
|
|
208
|
+
"ISC",
|
|
209
|
+
"dag",
|
|
210
|
+
"auction",
|
|
211
|
+
"None",
|
|
212
|
+
"2",
|
|
213
|
+
"0.75",
|
|
214
|
+
"true",
|
|
215
|
+
"false",
|
|
216
|
+
"0.88",
|
|
217
|
+
"/v1",
|
|
218
|
+
"reboot",
|
|
219
|
+
"snapshot",
|
|
220
|
+
"library book",
|
|
221
|
+
self.CLEAN_ENUM,
|
|
222
|
+
self.CLEAN_SINGLE_SEGMENT_ECHO,
|
|
223
|
+
)
|
|
224
|
+
for item in corpus:
|
|
225
|
+
with self.subTest(item=item):
|
|
226
|
+
d = self._probe([item])
|
|
227
|
+
self.assertTrue(d["threw"])
|
|
228
|
+
self.assertEqual(d["errors"], [item])
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class TestR2FifoHardeningPython(unittest.TestCase):
|
|
232
|
+
"""R2 (W17) PY: every config.json pre-read refuses FIFOs instead of hanging."""
|
|
233
|
+
|
|
234
|
+
def test_fifo_reads_do_not_hang(self):
|
|
235
|
+
# Hang-regression proof: pre-fix, each of these blocked forever on the
|
|
236
|
+
# open(). Run FIFO-touching reads in a child with a timeout.
|
|
237
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
238
|
+
os.mkfifo(os.path.join(tmp, "config.json"))
|
|
239
|
+
code = (
|
|
240
|
+
"import json, sys; "
|
|
241
|
+
"sys.path.insert(0, '.'); "
|
|
242
|
+
"from skills.swarm_config.configure import "
|
|
243
|
+
"load_config, heal_config, config_hash, snapshot; "
|
|
244
|
+
f"d = {json.dumps(tmp)}; "
|
|
245
|
+
"cfg = load_config(d); "
|
|
246
|
+
"assert isinstance(cfg, dict) and cfg.get('mode') == 'research', cfg.get('mode'); "
|
|
247
|
+
"assert heal_config(d) is False; "
|
|
248
|
+
"assert config_hash(d) is None; "
|
|
249
|
+
"snap = snapshot(d); "
|
|
250
|
+
"assert snap['hash'] is None and snap['config'].get('mode') == 'research'; "
|
|
251
|
+
"print(json.dumps({'ok': True})); "
|
|
252
|
+
)
|
|
253
|
+
res = subprocess.run(
|
|
254
|
+
[sys.executable, "-c", code],
|
|
255
|
+
capture_output=True,
|
|
256
|
+
text=True,
|
|
257
|
+
cwd=str(PROJECT_ROOT),
|
|
258
|
+
timeout=60,
|
|
259
|
+
)
|
|
260
|
+
self.assertEqual(res.returncode, 0, res.stderr)
|
|
261
|
+
self.assertTrue(json.loads(res.stdout.strip())["ok"])
|
|
262
|
+
|
|
263
|
+
def test_fifo_save_fails_closed(self):
|
|
264
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
265
|
+
os.mkfifo(os.path.join(tmp, "config.json"))
|
|
266
|
+
code = (
|
|
267
|
+
"import sys\n"
|
|
268
|
+
"sys.path.insert(0, '.')\n"
|
|
269
|
+
"from skills.swarm_config.configure import save_config, ConfigError\n"
|
|
270
|
+
f"d = {json.dumps(tmp)}\n"
|
|
271
|
+
"try:\n"
|
|
272
|
+
" save_config({'mode': 'audit'}, d)\n"
|
|
273
|
+
" print('SAVED')\n"
|
|
274
|
+
"except ConfigError as exc:\n"
|
|
275
|
+
" print('REFUSED:' + exc.code)\n"
|
|
276
|
+
)
|
|
277
|
+
res = subprocess.run(
|
|
278
|
+
[sys.executable, "-c", code],
|
|
279
|
+
capture_output=True,
|
|
280
|
+
text=True,
|
|
281
|
+
cwd=str(PROJECT_ROOT),
|
|
282
|
+
timeout=60,
|
|
283
|
+
)
|
|
284
|
+
self.assertEqual(res.returncode, 0, res.stderr)
|
|
285
|
+
self.assertTrue(
|
|
286
|
+
res.stdout.strip().startswith("REFUSED:CONFIG_UNREADABLE"),
|
|
287
|
+
res.stdout,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def test_normal_files_unchanged(self):
|
|
291
|
+
# Blast-radius pin: regular files behave byte-identically (hash over
|
|
292
|
+
# exact bytes, merge, round-trip).
|
|
293
|
+
from skills.swarm_config.configure import (
|
|
294
|
+
config_hash,
|
|
295
|
+
load_config,
|
|
296
|
+
save_config,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
300
|
+
raw = b'{"mode": "audit", "max_iterations": 3}'
|
|
301
|
+
(Path(tmp) / "config.json").write_bytes(raw)
|
|
302
|
+
self.assertEqual(config_hash(tmp), hashlib.sha256(raw).hexdigest())
|
|
303
|
+
cfg = load_config(tmp)
|
|
304
|
+
self.assertEqual(cfg["mode"], "audit")
|
|
305
|
+
self.assertEqual(cfg["max_iterations"], 3)
|
|
306
|
+
# Defaults still merge under a partial file.
|
|
307
|
+
self.assertEqual(cfg["search_engine"], "duckduckgo")
|
|
308
|
+
save_config({"search_engine": "searxng"}, tmp)
|
|
309
|
+
self.assertEqual(load_config(tmp)["search_engine"], "searxng")
|
|
310
|
+
self.assertEqual(load_config(tmp)["mode"], "audit")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class TestR2FifoHardeningJs(unittest.TestCase):
|
|
314
|
+
"""R2 (W17) JS: config-io readers refuse FIFOs; save fails closed."""
|
|
315
|
+
|
|
316
|
+
def test_fifo_reads_do_not_hang(self):
|
|
317
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
318
|
+
os.mkfifo(os.path.join(tmp, "config.json"))
|
|
319
|
+
res = run_node(
|
|
320
|
+
f"""
|
|
321
|
+
import {{ readConfigSnapshot, loadConfig, readRawConfig, saveConfig }} from "./plugins/opencode/config-io.js";
|
|
322
|
+
const dir = {json.dumps(tmp)};
|
|
323
|
+
const snap = readConfigSnapshot(dir);
|
|
324
|
+
const cfg = loadConfig(dir);
|
|
325
|
+
const raw = readRawConfig(dir);
|
|
326
|
+
let saved = "no-throw";
|
|
327
|
+
try {{
|
|
328
|
+
saveConfig({{ mode: "audit" }}, dir);
|
|
329
|
+
}} catch (e) {{
|
|
330
|
+
saved = e && e.code ? e.code : "threw";
|
|
331
|
+
}}
|
|
332
|
+
console.log(JSON.stringify({{
|
|
333
|
+
hash: snap.hash, malformed: snap.malformed, raw,
|
|
334
|
+
mode: cfg && cfg.mode, engine: cfg && cfg.search_engine,
|
|
335
|
+
saved,
|
|
336
|
+
}}));
|
|
337
|
+
""",
|
|
338
|
+
timeout=60,
|
|
339
|
+
)
|
|
340
|
+
self.assertEqual(res.returncode, 0, res.stderr)
|
|
341
|
+
d = last_json_object(res.stdout)
|
|
342
|
+
# FIFO refused: no hash, no raw, defaults, save refused — and the
|
|
343
|
+
# process returned instead of hanging.
|
|
344
|
+
self.assertIsNone(d["hash"])
|
|
345
|
+
self.assertIsNone(d["raw"])
|
|
346
|
+
self.assertEqual(d["mode"], "research")
|
|
347
|
+
self.assertEqual(d["engine"], "duckduckgo")
|
|
348
|
+
self.assertEqual(d["saved"], "CONFIG_UNREADABLE")
|
|
349
|
+
|
|
350
|
+
def test_normal_files_unchanged(self):
|
|
351
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
352
|
+
(Path(tmp) / "config.json").write_text(
|
|
353
|
+
'{"mode": "audit", "max_iterations": 3}', encoding="utf-8"
|
|
354
|
+
)
|
|
355
|
+
res = run_node(
|
|
356
|
+
f"""
|
|
357
|
+
import {{ readConfigSnapshot, loadConfig }} from "./plugins/opencode/config-io.js";
|
|
358
|
+
const dir = {json.dumps(tmp)};
|
|
359
|
+
const snap = readConfigSnapshot(dir);
|
|
360
|
+
const cfg = loadConfig(dir);
|
|
361
|
+
console.log(JSON.stringify({{ hash: snap.hash, mode: cfg.mode, depth: cfg.max_iterations, engine: cfg.search_engine }}));
|
|
362
|
+
""",
|
|
363
|
+
timeout=60,
|
|
364
|
+
)
|
|
365
|
+
self.assertEqual(res.returncode, 0, res.stderr)
|
|
366
|
+
d = last_json_object(res.stdout)
|
|
367
|
+
self.assertEqual(
|
|
368
|
+
d["hash"],
|
|
369
|
+
hashlib.sha256(b'{"mode": "audit", "max_iterations": 3}').hexdigest(),
|
|
370
|
+
)
|
|
371
|
+
self.assertEqual(d["mode"], "audit")
|
|
372
|
+
self.assertEqual(d["depth"], 3)
|
|
373
|
+
self.assertEqual(d["engine"], "duckduckgo")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class TestR3HealConfigDocs(unittest.TestCase):
|
|
377
|
+
"""R3 (W18): docstrings describe the true non-healing-read architecture."""
|
|
378
|
+
|
|
379
|
+
def test_configure_docstrings_state_non_healing_truth(self):
|
|
380
|
+
text = (PROJECT_ROOT / "skills" / "swarm_config" / "configure.py").read_text(
|
|
381
|
+
encoding="utf-8"
|
|
382
|
+
)
|
|
383
|
+
# The false live-healing claim is gone...
|
|
384
|
+
self.assertNotIn("the CLI and MCP surfaces call this", text)
|
|
385
|
+
self.assertNotIn("persists that migration on next touch", text)
|
|
386
|
+
# ...replaced by the non-healing truth naming every non-healing read.
|
|
387
|
+
self.assertIn("NO\n production read path calls this", text)
|
|
388
|
+
self.assertIn("non-healing", text)
|
|
389
|
+
for marker in ("`--show`", "`load_config`", "opportunistically"):
|
|
390
|
+
self.assertIn(marker, text)
|
|
391
|
+
|
|
392
|
+
def test_skill_doc_states_non_healing_truth(self):
|
|
393
|
+
text = (PROJECT_ROOT / "skills" / "swarm_config" / "SKILL.md").read_text(
|
|
394
|
+
encoding="utf-8"
|
|
395
|
+
)
|
|
396
|
+
self.assertIn("Non-healing reads", text)
|
|
397
|
+
self.assertIn("no production", text)
|
|
398
|
+
|
|
399
|
+
def test_heal_config_still_exported_and_safe(self):
|
|
400
|
+
from skills.swarm_config import heal_config
|
|
401
|
+
|
|
402
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
403
|
+
# Absent file: nothing to heal.
|
|
404
|
+
self.assertFalse(heal_config(tmp))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class TestR4GateSurface(unittest.TestCase):
|
|
408
|
+
"""R4 (W6): gate reachable through the iumbtems_factory tool surface."""
|
|
409
|
+
|
|
410
|
+
def test_handler_forwards_gate(self):
|
|
411
|
+
from runner.mcp_server import _handle_factory
|
|
412
|
+
|
|
413
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
414
|
+
init = json.loads(
|
|
415
|
+
_handle_factory(
|
|
416
|
+
{"command": "init", "run": "p08-gate", "project_dir": tmp}
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
self.assertEqual(init["status"], "ok")
|
|
420
|
+
opened = json.loads(
|
|
421
|
+
_handle_factory(
|
|
422
|
+
{
|
|
423
|
+
"command": "gate",
|
|
424
|
+
"action": "open",
|
|
425
|
+
"run": "p08-gate",
|
|
426
|
+
"phase": "08-waiver-reopens",
|
|
427
|
+
"project_dir": tmp,
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
self.assertEqual(opened["status"], "ok", opened)
|
|
432
|
+
self.assertEqual(opened["command"], "gate")
|
|
433
|
+
state = json.loads(
|
|
434
|
+
(Path(tmp) / ".factory" / "p08-gate" / "state.json").read_text(
|
|
435
|
+
encoding="utf-8"
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
self.assertEqual(state["gates"]["08-waiver-reopens"]["status"], "open")
|
|
439
|
+
counted = json.loads(
|
|
440
|
+
_handle_factory(
|
|
441
|
+
{
|
|
442
|
+
"command": "gate",
|
|
443
|
+
"action": "count",
|
|
444
|
+
"run": "p08-gate",
|
|
445
|
+
"phase": "08-waiver-reopens",
|
|
446
|
+
"project_dir": tmp,
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
self.assertEqual(counted["status"], "ok", counted)
|
|
451
|
+
settled = json.loads(
|
|
452
|
+
_handle_factory(
|
|
453
|
+
{
|
|
454
|
+
"command": "gate",
|
|
455
|
+
"action": "settle",
|
|
456
|
+
"run": "p08-gate",
|
|
457
|
+
"phase": "08-waiver-reopens",
|
|
458
|
+
"reason": "phase-08 receipt",
|
|
459
|
+
"project_dir": tmp,
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
self.assertEqual(settled["status"], "ok", settled)
|
|
464
|
+
state = json.loads(
|
|
465
|
+
(Path(tmp) / ".factory" / "p08-gate" / "state.json").read_text(
|
|
466
|
+
encoding="utf-8"
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
self.assertEqual(state["gates"]["08-waiver-reopens"]["status"], "settled")
|
|
470
|
+
|
|
471
|
+
def test_handler_rejects_bad_action_and_bad_command(self):
|
|
472
|
+
from runner.mcp_server import _handle_factory
|
|
473
|
+
|
|
474
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
475
|
+
_handle_factory(
|
|
476
|
+
{"command": "init", "run": "p08-gate-bad", "project_dir": tmp}
|
|
477
|
+
)
|
|
478
|
+
bad = json.loads(
|
|
479
|
+
_handle_factory(
|
|
480
|
+
{
|
|
481
|
+
"command": "gate",
|
|
482
|
+
"action": "frobnicate",
|
|
483
|
+
"run": "p08-gate-bad",
|
|
484
|
+
"phase": "x",
|
|
485
|
+
"project_dir": tmp,
|
|
486
|
+
}
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
# Invalid action: the helper exits 2 (argparse), which the handler
|
|
490
|
+
# surfaces as escalated/error — never ok.
|
|
491
|
+
self.assertIn(bad["status"], ("error", "escalated"), bad)
|
|
492
|
+
self.assertEqual(bad["returncode"], 2, bad)
|
|
493
|
+
with self.assertRaises(ValueError):
|
|
494
|
+
_handle_factory({"command": "launch", "project_dir": tmp})
|
|
495
|
+
|
|
496
|
+
def test_tool_spec_and_js_catalog_advertise_gate(self):
|
|
497
|
+
from runner.mcp_server import build_tools
|
|
498
|
+
|
|
499
|
+
spec = next(t for t in build_tools() if t.name == "iumbtems_factory")
|
|
500
|
+
schema = spec.input_schema
|
|
501
|
+
self.assertIn("gate", schema["properties"]["command"]["enum"])
|
|
502
|
+
self.assertIn(
|
|
503
|
+
"open",
|
|
504
|
+
schema["properties"]["action"]["enum"],
|
|
505
|
+
)
|
|
506
|
+
for action in ("settle", "approve", "waive", "escalate", "count"):
|
|
507
|
+
self.assertIn(action, schema["properties"]["action"]["enum"])
|
|
508
|
+
res = run_node(
|
|
509
|
+
"""
|
|
510
|
+
import { TOOL_CATALOG } from "./plugins/opencode/index.js";
|
|
511
|
+
const entry = TOOL_CATALOG.find((t) => t.name === "iumbtems_factory");
|
|
512
|
+
console.log(JSON.stringify({ input: entry.input, description: entry.description }));
|
|
513
|
+
"""
|
|
514
|
+
)
|
|
515
|
+
self.assertEqual(res.returncode, 0, res.stderr)
|
|
516
|
+
advertised = last_json_object(res.stdout)
|
|
517
|
+
self.assertIn("gate", advertised["input"]["properties"]["command"]["enum"])
|
|
518
|
+
for action in ("open", "settle", "approve", "waive", "escalate", "count"):
|
|
519
|
+
self.assertIn(action, advertised["input"]["properties"]["action"]["enum"])
|
|
520
|
+
self.assertIn("gate", advertised["description"])
|
|
521
|
+
# The Pi extension declares its own factory surface (thin passthrough
|
|
522
|
+
# to the same MCP tool): pin its enum by text — importing the Pi
|
|
523
|
+
# harness here would couple this suite to its host mocks.
|
|
524
|
+
pi_text = (PROJECT_ROOT / "extensions" / "pi" / "index.js").read_text(
|
|
525
|
+
encoding="utf-8"
|
|
526
|
+
)
|
|
527
|
+
pi_factory = pi_text[
|
|
528
|
+
pi_text.index("name: 'iumbtems_factory'") : pi_text.index(
|
|
529
|
+
"name: 'iumbtems_factory'"
|
|
530
|
+
)
|
|
531
|
+
+ 1500
|
|
532
|
+
]
|
|
533
|
+
self.assertIn("'gate'", pi_factory)
|
|
534
|
+
for action in ("open", "settle", "approve", "waive", "escalate", "count"):
|
|
535
|
+
self.assertIn(f"'{action}'", pi_factory)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
if __name__ == "__main__":
|
|
539
|
+
unittest.main()
|