@heretek-ai/epistemic-swarm 0.7.21 → 0.7.22
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 +1 -1
- package/plugins/antigravity/plugin.json +1 -1
- package/plugins/factory/skills/factory/SKILL.md +1 -1
- package/plugins/opencode/config-io.js +598 -31
- package/plugins/opencode/index.js +56 -15
- package/plugins/opencode/tui.js +115 -11
- 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_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_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_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/fixtures/config_validation_cases.json +60 -0
- package/runner/tests/test_config_schema.py +119 -15
- package/runner/tests/test_hardening_contract.py +391 -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/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()
|
|
@@ -104,12 +104,12 @@
|
|
|
104
104
|
"string",
|
|
105
105
|
"null"
|
|
106
106
|
],
|
|
107
|
-
"pattern": "^https?://",
|
|
107
|
+
"pattern": "^https?://[^\\s/]+\\S*$",
|
|
108
108
|
"default": null,
|
|
109
109
|
"x-label": "SearXNG URL",
|
|
110
110
|
"x-scope": "retrieval",
|
|
111
111
|
"x-restart": "next-run",
|
|
112
|
-
"description": "Base URL of a self-hosted SearXNG; exported to spawned children as SEARXNG_URL when set (preflight also reads the env var). Must include an http:// or https:// scheme; null leaves the environment untouched."
|
|
112
|
+
"description": "Base URL of a self-hosted SearXNG; exported to spawned children as SEARXNG_URL when set (preflight also reads the env var). Must include an http:// or https:// scheme AND a host (`http://` with no host is rejected); null leaves the environment untouched."
|
|
113
113
|
},
|
|
114
114
|
"license_whitelist": {
|
|
115
115
|
"type": "array",
|
|
@@ -154,11 +154,12 @@
|
|
|
154
154
|
"null"
|
|
155
155
|
],
|
|
156
156
|
"minLength": 1,
|
|
157
|
+
"pattern": "^\\S(.*\\S)?$",
|
|
157
158
|
"default": null,
|
|
158
159
|
"x-label": "Domain Pack",
|
|
159
160
|
"x-scope": "audit",
|
|
160
161
|
"x-restart": "next-run",
|
|
161
|
-
"description": "Regulated Domain Pack id (config/domain_packs/) or null for the legacy constitution. The empty string
|
|
162
|
+
"description": "Regulated Domain Pack id (config/domain_packs/) or null for the legacy constitution. The empty string, whitespace-only strings, and ids with leading/trailing whitespace are rejected; a pack id that does not resolve fails loudly when the constitution loads. UI entry points (iumbtems_set_domain_pack) strip surrounding whitespace before resolving."
|
|
162
163
|
},
|
|
163
164
|
"verify": {
|
|
164
165
|
"type": "object",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/skills/factory/SKILL.md
CHANGED
|
@@ -21,7 +21,7 @@ npm cache in consuming projects, and relative paths broke live: the manager
|
|
|
21
21
|
agent ran `find / -name factory.py`). The tool wraps the same helper and
|
|
22
22
|
resolves the project via `project_dir` argument, `IUMBTEMS_PROJECT_DIR`, or
|
|
23
23
|
the session cwd. It supports `init`, `phase-add`, `qa-record`, `expansion`,
|
|
24
|
-
`stop`, and returns `status: escalated` (exit 2) on the 3rd QA failure.
|
|
24
|
+
`stop`, `gate` (`--action open|settle|approve|waive|escalate|count`), and returns `status: escalated` (exit 2) on the 3rd QA failure.
|
|
25
25
|
|
|
26
26
|
## 2. Gate protocol (max 5 swarm cycles per gate)
|
|
27
27
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -17,6 +17,35 @@ from typing import Dict, Any, Optional, Tuple
|
|
|
17
17
|
DEFAULT_RESEARCH_DIR = ".research"
|
|
18
18
|
BASE_DIR_HELP = "Base .research directory"
|
|
19
19
|
|
|
20
|
+
#: Historical decision boundary for the fuzzy quote fallback (config
|
|
21
|
+
#: `verify.min_fuzzy_confidence`). This is the default whenever no threshold
|
|
22
|
+
#: is configured or the configured value is malformed.
|
|
23
|
+
DEFAULT_MIN_FUZZY_CONFIDENCE = 0.88
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def coerce_min_fuzzy_confidence(value: Any) -> float:
|
|
27
|
+
"""Coerce a `verify.min_fuzzy_confidence` candidate into [0, 1].
|
|
28
|
+
|
|
29
|
+
SINGLE-ENFORCEMENT-POINT CONTRACT (Phase 05 H5): this function is the one
|
|
30
|
+
definition of the coercion rule — string values are parsed with `float()`
|
|
31
|
+
(so a hand-edited `"0.95"` behaves like `0.95`), the result is clamped
|
|
32
|
+
into [0, 1], and anything malformed (None, non-numeric strings, NaN)
|
|
33
|
+
falls back to `DEFAULT_MIN_FUZZY_CONFIDENCE` (0.88, the strict
|
|
34
|
+
historical boundary). `SourceHasher.verify_quote` enforces it at call
|
|
35
|
+
time; every other layer (MCP `iumbtems_verify_quote`, the auditor, the
|
|
36
|
+
swarm runner) forwards its configured value untouched and must NOT
|
|
37
|
+
re-implement this rule. The persisted schema stays strict
|
|
38
|
+
(`type: number`): coercion exists only for hand-edited files and direct
|
|
39
|
+
library callers, never as an excuse to loosen validation.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
threshold = float(value)
|
|
43
|
+
except (TypeError, ValueError):
|
|
44
|
+
return DEFAULT_MIN_FUZZY_CONFIDENCE
|
|
45
|
+
if threshold != threshold: # NaN: float("nan") parses but matches nothing
|
|
46
|
+
return DEFAULT_MIN_FUZZY_CONFIDENCE
|
|
47
|
+
return max(0.0, min(1.0, threshold))
|
|
48
|
+
|
|
20
49
|
|
|
21
50
|
def _log_retrieval(base_dir, kind: str, **fields) -> None:
|
|
22
51
|
"""Best-effort retrieval telemetry (see runner/retrieval_log.py)."""
|
|
@@ -146,15 +175,11 @@ class SourceHasher:
|
|
|
146
175
|
the historical decision boundary; callers that pass a value opt into a
|
|
147
176
|
stricter/looser match. Exact and normalized matches are unaffected.
|
|
148
177
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
178
|
+
Coercion is NOT done here inline: `coerce_min_fuzzy_confidence` (the
|
|
179
|
+
single enforcement point — see its contract) parses, clamps, and
|
|
180
|
+
defaults the threshold.
|
|
152
181
|
"""
|
|
153
|
-
|
|
154
|
-
threshold = float(min_fuzzy_confidence)
|
|
155
|
-
except (TypeError, ValueError):
|
|
156
|
-
threshold = 0.88
|
|
157
|
-
threshold = max(0.0, min(1.0, threshold))
|
|
182
|
+
threshold = coerce_min_fuzzy_confidence(min_fuzzy_confidence)
|
|
158
183
|
|
|
159
184
|
source_text = self.get_source_content(content_hash)
|
|
160
185
|
if not source_text:
|
|
@@ -122,7 +122,12 @@ and enforced by `iumbtems_config` before any write. The write guard
|
|
|
122
122
|
prefix of **at least 8 hex characters**; conflicting spellings are rejected with
|
|
123
123
|
`CONFLICTING_EXPECTED_HASH`. `heal_config` refuses to persist a file that fails
|
|
124
124
|
canonical validation (the legacy-pin migration stays in memory until the
|
|
125
|
-
invalid values are fixed).
|
|
125
|
+
invalid values are fixed). Non-healing reads (phase-08 R3 truth): no production
|
|
126
|
+
read path calls `heal_config` — `--show` / bare CLI, the MCP `show` leg, and
|
|
127
|
+
`load_config` itself never write. The migration is applied in memory on every
|
|
128
|
+
load and persists opportunistically when a later write leg merges the loaded
|
|
129
|
+
config; `heal_config` remains an exported, tested, explicit repair helper for
|
|
130
|
+
persisting the migration outside any other write.
|
|
126
131
|
|
|
127
132
|
## 4. TUI Settings Surface (OpenCode V2)
|
|
128
133
|
|
|
Binary file
|