@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
|
@@ -34,6 +34,52 @@ import {
|
|
|
34
34
|
resolveExpectedHash,
|
|
35
35
|
validateConfig,
|
|
36
36
|
} from './config-io.js';
|
|
37
|
+
import {
|
|
38
|
+
createHookBus,
|
|
39
|
+
gatePreCommit,
|
|
40
|
+
registerHookBus,
|
|
41
|
+
} from './hook-bus.js';
|
|
42
|
+
import {
|
|
43
|
+
registerAnalysisBridge,
|
|
44
|
+
} from './analysis-bridge.js';
|
|
45
|
+
import {
|
|
46
|
+
registerDepHealth,
|
|
47
|
+
} from './dep-health.js';
|
|
48
|
+
import {
|
|
49
|
+
registerWeightSignals,
|
|
50
|
+
} from './weight-signals.js';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Shared hook bus (Phase 01-hook-bus-spec). Six blockable events with tiered
|
|
54
|
+
* fast-short/slow-long enforcement + audit log; fail-open, never throws.
|
|
55
|
+
*
|
|
56
|
+
* Phase evidence: transform-shaped registrations + the `tool.execute.before`
|
|
57
|
+
* gap [VERIFIED: sha256:52ac5b4d26062cfc6093440faa415ab152f8acd0d18c4496eb0087ed18a27d76
|
|
58
|
+
* `file:///home/john/Projects/IUMBTEMS/plugins/opencode/index.js`:1038-1126];
|
|
59
|
+
* setup adopt pattern [VERIFIED: same hash, `:2714-2750`].
|
|
60
|
+
*
|
|
61
|
+
* Per-event enforcement points (fix-round docs; see docs/HOOK_BUS_PARITY.md):
|
|
62
|
+
* - `pre-tool-use` / `post-tool-use`: ENFORCED in `registerHostTools`
|
|
63
|
+
* `execute` for this plugin's OWN tools — a fast/slow tier-table deny
|
|
64
|
+
* blocks execution (pre) or suppresses delivery (post). Host-NATIVE tools
|
|
65
|
+
* (webfetch/websearch) have no `tool.execute.before` point, so they are
|
|
66
|
+
* NOT gated — same gap class as the upstream ask, documented not implied.
|
|
67
|
+
* - `pre-commit`: ENFORCED on both write legs — the settings-RPC `set`
|
|
68
|
+
* (`createSettingsHandlers().set`) and the TUI direct-fs fallback
|
|
69
|
+
* (`gateDirectFsPreCommit` in `tui.js`). Stale-write guards stay downstream.
|
|
70
|
+
* - `session-start`: ADVISORY BY DESIGN — emitted in the compaction hook, a
|
|
71
|
+
* deny is audit-logged but never drops the state push. Never counted as a
|
|
72
|
+
* deny-blocks host path.
|
|
73
|
+
* - `stop` / `notification`: BUS-LEVEL ONLY — no host interception point
|
|
74
|
+
* exists yet, so deny-blocks holds at `bus.emit` (direct subscribers) and
|
|
75
|
+
* no host action is gated. No claim here implies host blocking.
|
|
76
|
+
*
|
|
77
|
+
* Caller-latency contract (GOAL §5/§6): `emit` runs tiers concurrently with
|
|
78
|
+
* each handler bounded by its tier budget, so a call resolves within
|
|
79
|
+
* max(fastBudget, slowBudget) + epsilon — a hung slow handler costs the
|
|
80
|
+
* caller at most the slow budget (default 5000 ms), never indefinitely.
|
|
81
|
+
*/
|
|
82
|
+
export const hookBus = createHookBus();
|
|
37
83
|
|
|
38
84
|
const __filename = fileURLToPath(import.meta.url);
|
|
39
85
|
const __dirname = path.dirname(__filename);
|
|
@@ -152,13 +198,13 @@ export const TOOL_CATALOG = [
|
|
|
152
198
|
show: {
|
|
153
199
|
type: 'boolean',
|
|
154
200
|
description:
|
|
155
|
-
'Inspect only; equivalent to calling with no update keys (reads are always safe).',
|
|
201
|
+
'Inspect only; equivalent to calling with no update keys (reads are always safe). When true, the call is read-only even if update keys are present (they are reported as ignored and nothing is written; the expected_hash guard, including the dual-spelling conflict check, is not evaluated).',
|
|
156
202
|
default: false,
|
|
157
203
|
},
|
|
158
204
|
expected_hash: {
|
|
159
205
|
type: 'string',
|
|
160
206
|
description:
|
|
161
|
-
'Optimistic-concurrency guard: SHA-256 (or a unique prefix of at least 8 hex chars) of the config file you read. A mismatch returns a structured stale error with a fresh snapshot and writes nothing. Alias: `expectedHash`; if both spellings are present they must be equal or the call is rejected.',
|
|
207
|
+
'Optimistic-concurrency guard: SHA-256 (or a unique prefix of at least 8 hex chars) of the config file you read. A mismatch returns a structured stale error with a fresh snapshot and writes nothing. Empty/whitespace-only spellings count as absent. Alias: `expectedHash`; if both spellings are present they must be equal after normalization (strip + lowercase) or the call is rejected.',
|
|
162
208
|
},
|
|
163
209
|
expectedHash: {
|
|
164
210
|
type: 'string',
|
|
@@ -207,8 +253,8 @@ export const TOOL_CATALOG = [
|
|
|
207
253
|
},
|
|
208
254
|
searxng_url: {
|
|
209
255
|
type: ['string', 'null'],
|
|
210
|
-
pattern: '^https?://',
|
|
211
|
-
description: 'Base URL of a self-hosted SearXNG, exported as SEARXNG_URL when set; must include an http:// or https:// scheme; null leaves the environment untouched.',
|
|
256
|
+
pattern: '^https?://[^\\s/]+\\S*$',
|
|
257
|
+
description: 'Base URL of a self-hosted SearXNG, exported as SEARXNG_URL when set; must include an http:// or https:// scheme and a host, with no whitespace anywhere; null leaves the environment untouched.',
|
|
212
258
|
},
|
|
213
259
|
license_whitelist: {
|
|
214
260
|
type: 'array',
|
|
@@ -227,7 +273,8 @@ export const TOOL_CATALOG = [
|
|
|
227
273
|
domain_pack: {
|
|
228
274
|
type: ['string', 'null'],
|
|
229
275
|
minLength: 1,
|
|
230
|
-
|
|
276
|
+
pattern: '^\\S(.*\\S)?$',
|
|
277
|
+
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.',
|
|
231
278
|
},
|
|
232
279
|
verify: {
|
|
233
280
|
type: 'object',
|
|
@@ -440,13 +487,18 @@ export const TOOL_CATALOG = [
|
|
|
440
487
|
{
|
|
441
488
|
name: 'iumbtems_factory',
|
|
442
489
|
description:
|
|
443
|
-
'Drive factory run state: init / phase-add / qa-record / expansion / stop. State goes to <project>/.factory and <project>/.roadmap; no helper-script path needed.',
|
|
490
|
+
'Drive factory run state: init / phase-add / qa-record / expansion / stop / gate. State goes to <project>/.factory and <project>/.roadmap; no helper-script path needed.',
|
|
444
491
|
input: {
|
|
445
492
|
type: 'object',
|
|
446
493
|
properties: {
|
|
447
494
|
command: {
|
|
448
495
|
type: 'string',
|
|
449
|
-
enum: ['init', 'phase-add', 'qa-record', 'expansion', 'stop'],
|
|
496
|
+
enum: ['init', 'phase-add', 'qa-record', 'expansion', 'stop', 'gate'],
|
|
497
|
+
},
|
|
498
|
+
action: {
|
|
499
|
+
type: 'string',
|
|
500
|
+
enum: ['open', 'settle', 'approve', 'waive', 'escalate', 'count'],
|
|
501
|
+
description: 'Gate action (with command: gate)',
|
|
450
502
|
},
|
|
451
503
|
run: { type: 'string', description: 'Factory run name' },
|
|
452
504
|
phase: { type: 'string', description: 'Phase id (e.g. 01-auth)' },
|
|
@@ -1132,11 +1184,55 @@ async function registerHostTools(host) {
|
|
|
1132
1184
|
input: spec.input,
|
|
1133
1185
|
options: { codemode: false },
|
|
1134
1186
|
execute: async (args = {}, toolContext = undefined) => {
|
|
1187
|
+
const normArgs = normalizeArgs(name, args);
|
|
1188
|
+
const cwd = toolContext?.cwd || hostRoot;
|
|
1189
|
+
// Phase 01-hook-bus-spec (fix round): `pre-tool-use` gates this
|
|
1190
|
+
// plugin's OWN tool executions. A tier-table deny BLOCKS the
|
|
1191
|
+
// `callMcp` dispatch (structured `{content}` denial, same shape as
|
|
1192
|
+
// a result, so the host contract is unchanged); bus faults fail
|
|
1193
|
+
// open. Host-NATIVE tools are NOT gated here — there is no
|
|
1194
|
+
// `tool.execute.before` point (see the accepted-asymmetry note
|
|
1195
|
+
// above); that gap is documented in docs/HOOK_BUS_PARITY.md, never
|
|
1196
|
+
// implied away.
|
|
1197
|
+
let pre = { allowed: true };
|
|
1198
|
+
try {
|
|
1199
|
+
const gated = await hookBus.emit('pre-tool-use', {
|
|
1200
|
+
tool: name,
|
|
1201
|
+
args: normArgs,
|
|
1202
|
+
sessionID: toolContext?.sessionID ?? null,
|
|
1203
|
+
eventId: toolContext?.eventId ?? null,
|
|
1204
|
+
});
|
|
1205
|
+
if (gated && gated.allowed === false) pre = { allowed: false };
|
|
1206
|
+
} catch {
|
|
1207
|
+
pre = { allowed: true };
|
|
1208
|
+
}
|
|
1209
|
+
if (pre.allowed === false) {
|
|
1210
|
+
return { content: `Tool execution denied by pre-tool-use hook (tool: ${name}).` };
|
|
1211
|
+
}
|
|
1135
1212
|
const r = await callMcp(
|
|
1136
1213
|
name,
|
|
1137
|
-
|
|
1138
|
-
|
|
1214
|
+
normArgs,
|
|
1215
|
+
cwd
|
|
1139
1216
|
);
|
|
1217
|
+
// `post-tool-use`: the call already ran, so a tier-table deny
|
|
1218
|
+
// blocks DELIVERY — the real result is suppressed and a structured
|
|
1219
|
+
// `{content}` notice is returned instead (audit-logged either way).
|
|
1220
|
+
let post = { allowed: true };
|
|
1221
|
+
try {
|
|
1222
|
+
const gated = await hookBus.emit('post-tool-use', {
|
|
1223
|
+
tool: name,
|
|
1224
|
+
args: normArgs,
|
|
1225
|
+
result: (r && r.content) ?? null,
|
|
1226
|
+
sessionID: toolContext?.sessionID ?? null,
|
|
1227
|
+
eventId: toolContext?.eventId ?? null,
|
|
1228
|
+
});
|
|
1229
|
+
if (gated && gated.allowed === false) post = { allowed: false };
|
|
1230
|
+
} catch {
|
|
1231
|
+
post = { allowed: true };
|
|
1232
|
+
}
|
|
1233
|
+
if (post.allowed === false) {
|
|
1234
|
+
return { content: `Tool result suppressed by post-tool-use hook (tool: ${name}).` };
|
|
1235
|
+
}
|
|
1140
1236
|
return { content: r.content };
|
|
1141
1237
|
},
|
|
1142
1238
|
});
|
|
@@ -1750,6 +1846,16 @@ async function registerCompactionHook(host, context) {
|
|
|
1750
1846
|
const root =
|
|
1751
1847
|
event?.directory || host.location?.directory || context.location?.directory || event?.cwd;
|
|
1752
1848
|
if (!root) return;
|
|
1849
|
+
// Phase 01-hook-bus-spec: `session-start(compaction)` restores
|
|
1850
|
+
// `.research` state path. The bus verdict is advisory here — a deny
|
|
1851
|
+
// must not drop the compaction context push (fail-open); the emit is
|
|
1852
|
+
// still audit-logged on the shared bus.
|
|
1853
|
+
// [VERIFIED: sha256:52ac5b4d26062cfc6093440faa415ab152f8acd0d18c4496eb0087ed18a27d76 `:1750-1775`]
|
|
1854
|
+
try {
|
|
1855
|
+
await hookBus.emit('session-start', { root, eventId: 'compaction' });
|
|
1856
|
+
} catch {
|
|
1857
|
+
/* bus faults fail open; the state push below still runs */
|
|
1858
|
+
}
|
|
1753
1859
|
const text = buildCompactionContext(root);
|
|
1754
1860
|
if (!text) return;
|
|
1755
1861
|
const system = Array.isArray(event?.system) ? event.system : null;
|
|
@@ -2227,13 +2333,16 @@ function rpcFail(context, type, message, data) {
|
|
|
2227
2333
|
const SETTINGS_CONTROL_ARGS = new Set(['base_dir', 'dir', 'show', 'expected_hash', 'expectedHash']);
|
|
2228
2334
|
|
|
2229
2335
|
/**
|
|
2230
|
-
* Leak signatures that must never reach the RPC transport (phase-03 R7/R9
|
|
2231
|
-
* tracebacks, `File "…", line N` frames, interpreter failure
|
|
2232
|
-
* absolute filesystem paths of ANY root — POSIX
|
|
2233
|
-
* `/srv/…`, `/Users/…`, `/app/…`),
|
|
2234
|
-
*
|
|
2235
|
-
*
|
|
2236
|
-
*
|
|
2336
|
+
* Leak signatures that must never reach the RPC transport (phase-03 R7/R9,
|
|
2337
|
+
* phase-08 R1): tracebacks, `File "…", line N` frames, interpreter failure
|
|
2338
|
+
* lines, `file://` URIs, and absolute filesystem paths of ANY root — POSIX
|
|
2339
|
+
* multi-segment (e.g. `/workspace/…`, `/srv/…`, `/Users/…`, `/app/…`),
|
|
2340
|
+
* single-segment system roots (`/etc`, `/tmp`, `/lib`, `/lib64`, `/boot`,
|
|
2341
|
+
* `/snap`, macOS `/System`, `/Library`, `/Volumes`, `/Applications`, …),
|
|
2342
|
+
* Windows drive
|
|
2343
|
+
* (`C:\Users\…`), and UNC (`\\server\share`). Structured validation problems
|
|
2344
|
+
* (e.g. `mode: value … not in enum …`, type/range violations carrying
|
|
2345
|
+
* ordinary values) match none of these and pass through intact.
|
|
2237
2346
|
*/
|
|
2238
2347
|
const SETTINGS_ERROR_LEAK_PATTERNS = [
|
|
2239
2348
|
// Tracebacks and Python/Node interpreter failure lines.
|
|
@@ -2241,7 +2350,39 @@ const SETTINGS_ERROR_LEAK_PATTERNS = [
|
|
|
2241
2350
|
/File\s+["'][^"']+["']\s*,\s*line\s+\d+/i,
|
|
2242
2351
|
/\b(?:ModuleNotFoundError|ImportError|SyntaxError|NameError|OSError|Errno \d+)\b/,
|
|
2243
2352
|
// Absolute POSIX path of any root: a bounded leading slash + >=2 segments.
|
|
2244
|
-
|
|
2353
|
+
// Phase-08 R1 (W9) REWORK-2 L2: prefix class gains `[`, `{`, `-` so
|
|
2354
|
+
// bracket/brace/dash-delimited absolutes (`[/etc]`, `{/etc}`, `-/etc`,
|
|
2355
|
+
// `x-/lib64,y`) redact. Canonical values contain no `/`-root tokens
|
|
2356
|
+
// (proven by the canonical-value corpus passing verbatim), so `-`-prefixed
|
|
2357
|
+
// path tokens are disclosures, not user echoes.
|
|
2358
|
+
/(?:^|[\s"'(=:,\[{-])\/(?:[A-Za-z0-9._-]+\/)+[A-Za-z0-9._-]+/,
|
|
2359
|
+
// Phase-08 R1 (W9) REWORK-1: single-segment system-root absolutes
|
|
2360
|
+
// (`/etc`, `/tmp`, plus `/lib`, `/lib64`, `/boot`, `/snap`, macOS `/System`,
|
|
2361
|
+
// `/Library`, `/Volumes`, `/Applications`).
|
|
2362
|
+
// The >=2-segment arm above lets these through, but a bare system root in
|
|
2363
|
+
// a failure item is a local-filesystem disclosure (interpreter stderr
|
|
2364
|
+
// echoes absolute paths of any depth). The root allowlist keeps ordinary
|
|
2365
|
+
// user echoes intact: single-segment non-system values (`/v1`, `/nope`)
|
|
2366
|
+
// still pass through, and sanitization stays per-item (R9).
|
|
2367
|
+
// RESIDUAL (documented): truly exotic single-segment roots outside the
|
|
2368
|
+
// mainstream Linux + macOS set (e.g. `/net`, `/nfs`, future mount points)
|
|
2369
|
+
// still pass through by design to avoid over-redacting user echoes.
|
|
2370
|
+
// TRIGGER (consumed for this class): qa-b repro `["error at /lib nope"]`
|
|
2371
|
+
// passthrough + inspection-named `/lib64`, `/boot`, `/snap`, `/System`,
|
|
2372
|
+
// `/Library`, `/Volumes`, `/Applications` are now covered. Revisit only
|
|
2373
|
+
// with a NEW real leak repro naming the still-uncovered root.
|
|
2374
|
+
// Phase-08 R1 (W9) REWORK-2 L1: suffix lookahead gains `.!?]}` so
|
|
2375
|
+
// bare-root + sentence punctuation (`/lib.`, `/etc.`, `/lib!`, `/tmp?`,
|
|
2376
|
+
// `/lib]`, `/lib}`) redacts. The >=2-segment arm is untouched (its trailing
|
|
2377
|
+
// `[A-Za-z0-9._-]+` already consumes `/etc/foo.`). L2: prefix class gains
|
|
2378
|
+
// `[`, `{`, `-` (same `-` safety proof as above) so `[/etc]`, `[/lib]`,
|
|
2379
|
+
// `[/lib64]`, `{/etc}`, `-/etc`, `x-/lib64,y` redact.
|
|
2380
|
+
/(?:^|[\s"'(=:,\[{-])\/(?:etc|var|tmp|srv|app|home|root|usr|opt|bin|sbin|proc|sys|dev|run|mnt|media|private|Users|workspace|lib64|lib|boot|snap|System|Library|Volumes|Applications)(?=\/|$|[\s"'<>|,;:().!?\]}])/,
|
|
2381
|
+
// Phase-08 R1 (W9): `file://` URIs always disclose local filesystem
|
|
2382
|
+
// layout, and no canonical config value is a file:// URI (`searxng_url`
|
|
2383
|
+
// requires an http(s) scheme) — so a file:// URI in a failure item is
|
|
2384
|
+
// never a legitimate user echo. Fail closed.
|
|
2385
|
+
/file:\/\/[^\s"'<>|]+/i,
|
|
2245
2386
|
// Windows drive-letter absolute path (`C:\Users\…`) or UNC share (`\\srv\share`).
|
|
2246
2387
|
/\b[A-Za-z]:[\\/](?:[^\\/\s"'<>|]+[\\/])*[^\\/\s"'<>|]+/,
|
|
2247
2388
|
/\\\\[^\\/\s"'<>|]+[\\/][^\\/\s"'<>|]+/,
|
|
@@ -2289,6 +2430,11 @@ export function createSettingsHandlers(opts = {}) {
|
|
|
2289
2430
|
const storage = opts.storage || null;
|
|
2290
2431
|
const emitChanged = opts.emitChanged || null;
|
|
2291
2432
|
const reloadMcp = opts.reloadMcp || null;
|
|
2433
|
+
// Phase 01-hook-bus-spec: `pre-commit` gates config writes via this
|
|
2434
|
+
// settings-RPC path. Defaults to the shared bus; stale-write guards below
|
|
2435
|
+
// are preserved (a bus allow still runs the expected-hash MCP write, which
|
|
2436
|
+
// enforces staleness).
|
|
2437
|
+
const settingsBus = opts.hookBus || hookBus;
|
|
2292
2438
|
|
|
2293
2439
|
return {
|
|
2294
2440
|
get: async () => {
|
|
@@ -2377,6 +2523,29 @@ export function createSettingsHandlers(opts = {}) {
|
|
|
2377
2523
|
}
|
|
2378
2524
|
const args = { ...sanitized, base_dir: researchDir };
|
|
2379
2525
|
if (expected !== null) args.expected_hash = expected;
|
|
2526
|
+
// Phase 01-hook-bus-spec `pre-commit` gate: a tier-table deny blocks the
|
|
2527
|
+
// write (structured `invalid`, nothing written); bus faults fail open.
|
|
2528
|
+
// The expected-hash stale guard below still runs on every allow, so
|
|
2529
|
+
// stale-write preservation is unchanged. NOTE: the deny-block `rpcFail`
|
|
2530
|
+
// stays OUTSIDE the fail-open try — `rpcFail` throws when the host
|
|
2531
|
+
// supplies no `context.error`, and catching our own block would
|
|
2532
|
+
// silently fail open past a deny.
|
|
2533
|
+
let gate = null;
|
|
2534
|
+
try {
|
|
2535
|
+
gate = await gatePreCommit(settingsBus, {
|
|
2536
|
+
updates: sanitized,
|
|
2537
|
+
root,
|
|
2538
|
+
...(expected !== null ? { expected_hash: expected } : {}),
|
|
2539
|
+
});
|
|
2540
|
+
} catch {
|
|
2541
|
+
gate = { allowed: true };
|
|
2542
|
+
}
|
|
2543
|
+
if (gate && gate.allowed === false) {
|
|
2544
|
+
return rpcFail(context, 'invalid', 'Config write denied by pre-commit hook.', {
|
|
2545
|
+
written: false,
|
|
2546
|
+
errors: ['pre-commit hook denied the write'],
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2380
2549
|
const res = await callMcpImpl('iumbtems_config', args, root);
|
|
2381
2550
|
const payload = parseMcpContent(res);
|
|
2382
2551
|
if (payload && payload.status === 'stale') {
|
|
@@ -2461,6 +2630,7 @@ export async function registerSettingsRpc(host, opts = {}) {
|
|
|
2461
2630
|
storage,
|
|
2462
2631
|
emitChanged,
|
|
2463
2632
|
reloadMcp,
|
|
2633
|
+
hookBus: opts.hookBus || hookBus,
|
|
2464
2634
|
});
|
|
2465
2635
|
registration = await register.call(host.rpc, SettingsRpc, handlers);
|
|
2466
2636
|
if (!registration) return null;
|
|
@@ -2716,6 +2886,52 @@ export function createOpenCodePlugin(context = {}) {
|
|
|
2716
2886
|
} catch (err) {
|
|
2717
2887
|
log(host, 'error', 'registering temperature hook failed', errDetail(err));
|
|
2718
2888
|
}
|
|
2889
|
+
// Phase 01-hook-bus-spec: adopt the six-event hook bus alongside the
|
|
2890
|
+
// existing registrations (same adopt/best-effort pattern). The bus
|
|
2891
|
+
// registrar returns a single registration (or null), so wrap it for
|
|
2892
|
+
// `adopt`, which takes a list. Never throws.
|
|
2893
|
+
try {
|
|
2894
|
+
adopt([await registerHookBus(host, { bus: hookBus })]);
|
|
2895
|
+
} catch (err) {
|
|
2896
|
+
log(host, 'error', 'registering hook bus failed', errDetail(err));
|
|
2897
|
+
}
|
|
2898
|
+
// Phase 02-lsp-bridge: adopt the first bus consumer (CLI-based JS/TS
|
|
2899
|
+
// analysis, all-advisory) alongside the existing registrations (same
|
|
2900
|
+
// adopt/best-effort pattern). Analyzer-missing degrades to a silent
|
|
2901
|
+
// skip inside the bridge; this adoption never throws.
|
|
2902
|
+
try {
|
|
2903
|
+
adopt([await registerAnalysisBridge(host, { bus: hookBus })]);
|
|
2904
|
+
} catch (err) {
|
|
2905
|
+
log(host, 'error', 'registering analysis bridge failed', errDetail(err));
|
|
2906
|
+
}
|
|
2907
|
+
// Phase 03-dep-health-gate: adopt the second bus consumer
|
|
2908
|
+
// (outdated/vulnerable-dependency warnings, all-advisory + mechanical
|
|
2909
|
+
// QA coverage gate) alongside the existing registrations (same
|
|
2910
|
+
// adopt/best-effort pattern). Feed outages degrade to recorded
|
|
2911
|
+
// `unavailable` entries inside the module; this adoption never throws.
|
|
2912
|
+
try {
|
|
2913
|
+
adopt([await registerDepHealth(host, {
|
|
2914
|
+
bus: hookBus,
|
|
2915
|
+
// Host-reported project directory; the module falls back to the
|
|
2916
|
+
// process cwd when the host reports none (see checkDeps).
|
|
2917
|
+
root: host.location?.directory || undefined,
|
|
2918
|
+
})]);
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
log(host, 'error', 'registering dep-health failed', errDetail(err));
|
|
2921
|
+
}
|
|
2922
|
+
// Phase 04-coverage-treeshake-signals: adopt the third bus consumer
|
|
2923
|
+
// (coverage deltas + esbuild-measured bundle weight, all-advisory,
|
|
2924
|
+
// never-blocking) alongside the existing registrations (same
|
|
2925
|
+
// adopt/best-effort pattern). esbuild missing/failing degrades to a
|
|
2926
|
+
// recorded `unavailable` entry inside the module; this adoption never
|
|
2927
|
+
// throws.
|
|
2928
|
+
try {
|
|
2929
|
+
adopt([await registerWeightSignals(host, {
|
|
2930
|
+
bus: hookBus,
|
|
2931
|
+
})]);
|
|
2932
|
+
} catch (err) {
|
|
2933
|
+
log(host, 'error', 'registering weight-signals failed', errDetail(err));
|
|
2934
|
+
}
|
|
2719
2935
|
// Slow work AFTER registration.
|
|
2720
2936
|
try {
|
|
2721
2937
|
await applyConfigOptions(host, opts, context);
|
|
@@ -2733,14 +2949,31 @@ export function createOpenCodePlugin(context = {}) {
|
|
|
2733
2949
|
// `TypeError: de is not a function`, dropping every registration.
|
|
2734
2950
|
return async () => {
|
|
2735
2951
|
let disposed = 0;
|
|
2952
|
+
const pending = [];
|
|
2736
2953
|
for (const registration of registrations) {
|
|
2737
2954
|
try {
|
|
2738
|
-
|
|
2739
|
-
|
|
2955
|
+
const done = registration.dispose();
|
|
2956
|
+
if (done && typeof done.then === 'function') {
|
|
2957
|
+
// Async teardown settles alongside the rest; a rejection logs
|
|
2958
|
+
// exactly like a synchronous throw below.
|
|
2959
|
+
pending.push(
|
|
2960
|
+
done.then(
|
|
2961
|
+
() => { disposed += 1; },
|
|
2962
|
+
(err) => log(host, 'warn', 'registration.dispose() failed', errDetail(err)),
|
|
2963
|
+
),
|
|
2964
|
+
);
|
|
2965
|
+
} else {
|
|
2966
|
+
disposed += 1;
|
|
2967
|
+
}
|
|
2740
2968
|
} catch (err) {
|
|
2741
2969
|
log(host, 'warn', 'registration.dispose() failed', errDetail(err));
|
|
2742
2970
|
}
|
|
2743
2971
|
}
|
|
2972
|
+
// Await only when at least one disposal is asynchronous: awaiting an
|
|
2973
|
+
// already-settled promise still yields, which would defer the
|
|
2974
|
+
// synchronous nudge-stop loop below past a non-awaited cleanup() call
|
|
2975
|
+
// (pinned by test_setup_cleanup_aborts_nudge_streams).
|
|
2976
|
+
if (pending.length > 0) await Promise.all(pending);
|
|
2744
2977
|
registrations.length = 0;
|
|
2745
2978
|
let stopped = 0;
|
|
2746
2979
|
for (const stop of setupCleanups) {
|