ruvnet-brain 4.5.13 → 4.5.15
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/README.md +1 -1
- package/bin/install.mjs +124 -21
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/completion-claim-evidence.mjs +5 -1
- package/plugin/scripts/continuation-gate.mjs +17 -28
- package/plugin/scripts/continuation-objective.mjs +1 -1
- package/plugin/scripts/hook-shim.mjs +10 -0
- package/scripts/model-managed-prompt.mjs +56 -13
- package/scripts/model-routing-execution-adapters.mjs +74 -13
- package/scripts/release-qualification-contract.mjs +21 -10
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
|
|
|
7
7
|
|
|
8
8
|
# 🧠 RuvNet Brain
|
|
9
9
|
|
|
10
|
-
### 🧠 RuvNet Brain — [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
11
11
|
|
|
12
12
|
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
13
13
|
|
package/bin/install.mjs
CHANGED
|
@@ -1750,6 +1750,42 @@ function atomicReplace(targetPath, writeTmp) {
|
|
|
1750
1750
|
}
|
|
1751
1751
|
}
|
|
1752
1752
|
|
|
1753
|
+
function legacyBrainWrapperOwnership(wrapperPath, home) {
|
|
1754
|
+
const source = path.join(REPO_ROOT, 'plugin', 'scripts', 'codex-hook-wrapper.mjs');
|
|
1755
|
+
try {
|
|
1756
|
+
const canonicalHome = fs.realpathSync(home);
|
|
1757
|
+
if (path.resolve(wrapperPath) !== path.resolve(home, '.cache', 'ruvnet-brain', 'codex-hook.mjs')) {
|
|
1758
|
+
return { state: 'conflict', reason: 'bridge is outside the expected Brain namespace' };
|
|
1759
|
+
}
|
|
1760
|
+
for (const relative of ['.cache', path.join('.cache', 'ruvnet-brain')]) {
|
|
1761
|
+
const ancestor = path.join(home, relative);
|
|
1762
|
+
let parent;
|
|
1763
|
+
try { parent = fs.lstatSync(ancestor); } catch (error) {
|
|
1764
|
+
if (error.code === 'ENOENT') break;
|
|
1765
|
+
throw error;
|
|
1766
|
+
}
|
|
1767
|
+
if (!parent.isDirectory() || parent.isSymbolicLink()
|
|
1768
|
+
|| fs.realpathSync(ancestor) !== path.join(canonicalHome, relative)
|
|
1769
|
+
|| (typeof process.getuid === 'function' && parent.uid !== process.getuid())) {
|
|
1770
|
+
return { state: 'conflict', reason: 'bridge namespace is indirect or belongs to another owner' };
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
} catch { return { state: 'conflict', reason: 'bridge namespace could not be verified' }; }
|
|
1774
|
+
let stat;
|
|
1775
|
+
try { stat = fs.lstatSync(wrapperPath); } catch (error) {
|
|
1776
|
+
if (error.code === 'ENOENT') return { state: 'absent' };
|
|
1777
|
+
return { state: 'conflict', reason: 'existing bridge could not be inspected' };
|
|
1778
|
+
}
|
|
1779
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1
|
|
1780
|
+
|| (typeof process.getuid === 'function' && stat.uid !== process.getuid())) {
|
|
1781
|
+
return { state: 'conflict', reason: 'existing bridge is not an unshared regular file' };
|
|
1782
|
+
}
|
|
1783
|
+
try {
|
|
1784
|
+
if (fs.readFileSync(wrapperPath).equals(fs.readFileSync(source))) return { state: 'verified' };
|
|
1785
|
+
} catch { /* Missing proof is not ownership. */ }
|
|
1786
|
+
return { state: 'conflict', reason: 'existing bridge differs from the shipped Brain source' };
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1753
1789
|
export function wireCodexHost({
|
|
1754
1790
|
codexDir = codexHomeDir(),
|
|
1755
1791
|
configPath = path.join(codexDir, 'config.toml'),
|
|
@@ -1763,6 +1799,14 @@ export function wireCodexHost({
|
|
|
1763
1799
|
// No Codex on this machine is not a warning. Say nothing and change nothing.
|
|
1764
1800
|
if (!host) return { host: false, action: 'no-host' };
|
|
1765
1801
|
|
|
1802
|
+
// Check ownership before retiring registrations or changing any installed bytes.
|
|
1803
|
+
const wrapperOwnership = legacyBrainWrapperOwnership(hookWrapperPath, path.dirname(codexDir));
|
|
1804
|
+
if (wrapperOwnership.state === 'conflict') {
|
|
1805
|
+
if (announce) warn(`Codex hook bridge preserved: ${wrapperOwnership.reason}`);
|
|
1806
|
+
return { host: true, action: 'hook-wrapper-ownership-conflict', hookWrapperInstalled: false,
|
|
1807
|
+
error: wrapperOwnership.reason };
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1766
1810
|
if (announce) {
|
|
1767
1811
|
step('Wiring the Codex host', 'Codex reads MCP servers from ~/.codex/config.toml — the brain was never registered there');
|
|
1768
1812
|
}
|
|
@@ -1819,12 +1863,12 @@ export function wireCodexHost({
|
|
|
1819
1863
|
// the versioned cache so a plugin refresh cannot leave Codex pointing at a missing executable.
|
|
1820
1864
|
// The bridge resolves the active immutable generation on every fire and fails open on every
|
|
1821
1865
|
// non-contract error; only the two continuity registrations use it.
|
|
1822
|
-
retireManagedHookRegistrations({ home: path.dirname(codexDir), codexDir, wrapperPath: hookWrapperPath });
|
|
1823
1866
|
const hookWrapperSource = path.join(REPO_ROOT, 'plugin', 'scripts', 'codex-hook-wrapper.mjs');
|
|
1824
1867
|
if (!fs.existsSync(hookWrapperSource)) {
|
|
1825
1868
|
if (announce) warn(`Codex hook bridge source missing from this bundle: ${hookWrapperSource}`);
|
|
1826
1869
|
return { host: true, action: 'hook-wrapper-source-missing' };
|
|
1827
1870
|
}
|
|
1871
|
+
let hookRetirement;
|
|
1828
1872
|
try {
|
|
1829
1873
|
fs.mkdirSync(path.dirname(hookWrapperPath), { recursive: true });
|
|
1830
1874
|
const wrapperStat = (() => { try { return fs.lstatSync(hookWrapperPath); } catch (error) {
|
|
@@ -1833,11 +1877,17 @@ export function wireCodexHost({
|
|
|
1833
1877
|
if (wrapperStat?.isSymbolicLink()) {
|
|
1834
1878
|
throw new Error('installer-owned hook bridge path is a symlink; refusing to overwrite it');
|
|
1835
1879
|
}
|
|
1880
|
+
const currentOwnership = legacyBrainWrapperOwnership(hookWrapperPath, path.dirname(codexDir));
|
|
1881
|
+
if (currentOwnership.state === 'conflict') throw new Error(currentOwnership.reason);
|
|
1882
|
+
hookRetirement = retireManagedHookRegistrations({ home: path.dirname(codexDir), codexDir, wrapperPath: hookWrapperPath });
|
|
1836
1883
|
atomicReplace(hookWrapperPath, (tmp) => fs.copyFileSync(hookWrapperSource, tmp));
|
|
1837
1884
|
} catch (error) {
|
|
1838
1885
|
if (announce) warn(`could not install the Codex hook bridge at ${hookWrapperPath}: ${error.message}`);
|
|
1839
1886
|
return { host: true, action: 'hook-wrapper-install-failed', error: error.message };
|
|
1840
1887
|
}
|
|
1888
|
+
if (announce && hookRetirement.conflicts.length) {
|
|
1889
|
+
warn(`Preserved ${hookRetirement.conflicts.length} customized or unverified hook entries.`);
|
|
1890
|
+
}
|
|
1841
1891
|
|
|
1842
1892
|
let before = '';
|
|
1843
1893
|
try { before = fs.readFileSync(configPath, 'utf8'); } catch { /* first run — no config yet */ }
|
|
@@ -1848,7 +1898,7 @@ export function wireCodexHost({
|
|
|
1848
1898
|
info(` to hand it to us instead, delete that ${c.bold('[mcp_servers.ruvnet-brain]')} block and re-run this installer`);
|
|
1849
1899
|
}
|
|
1850
1900
|
return { host: true, action, serverPath, managedCliPath, runtimePreferencesPath,
|
|
1851
|
-
hookWrapperPath, hookWrapperInstalled: true };
|
|
1901
|
+
hookWrapperPath, hookWrapperInstalled: true, hookOwnershipConflicts: hookRetirement.conflicts.length };
|
|
1852
1902
|
}
|
|
1853
1903
|
if (text !== before) {
|
|
1854
1904
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
@@ -1860,7 +1910,8 @@ export function wireCodexHost({
|
|
|
1860
1910
|
info(` ${c.dim('only our marked block is written; every other section is byte-preserved')}`);
|
|
1861
1911
|
}
|
|
1862
1912
|
return { host: true, action, serverPath, managedCliPath, runtimePreferencesPath,
|
|
1863
|
-
hookWrapperPath, hookWrapperInstalled: true,
|
|
1913
|
+
hookWrapperPath, hookWrapperInstalled: true, hookOwnershipConflicts: hookRetirement.conflicts.length,
|
|
1914
|
+
changed: text !== before };
|
|
1864
1915
|
}
|
|
1865
1916
|
|
|
1866
1917
|
const CODEX_PLUGIN_ID = 'ruvnet-brain@ruvnet-brain';
|
|
@@ -2140,8 +2191,18 @@ export function codexSessionSafety(status) {
|
|
|
2140
2191
|
};
|
|
2141
2192
|
}
|
|
2142
2193
|
|
|
2194
|
+
function codexAdministrativeProbeBinary() {
|
|
2195
|
+
if (process.env.CODEX_BIN) return process.env.CODEX_BIN;
|
|
2196
|
+
try {
|
|
2197
|
+
const brainHome = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
|
|
2198
|
+
const configured = JSON.parse(fs.readFileSync(path.join(brainHome, 'model-routing', 'terminal-launcher-config.json'), 'utf8'));
|
|
2199
|
+
if (typeof configured.realCodex === 'string' && path.isAbsolute(configured.realCodex)) return configured.realCodex;
|
|
2200
|
+
} catch { /* absent or malformed configuration retains the ordinary bounded PATH probe */ }
|
|
2201
|
+
return 'codex';
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2143
2204
|
function codexHooksList({
|
|
2144
|
-
codexBin =
|
|
2205
|
+
codexBin = codexAdministrativeProbeBinary(),
|
|
2145
2206
|
codexHome = codexHomeDir(),
|
|
2146
2207
|
cwd = process.cwd(),
|
|
2147
2208
|
timeoutMs = 8_000,
|
|
@@ -2316,23 +2377,62 @@ export function codexLifecycleGuidance(status) {
|
|
|
2316
2377
|
}
|
|
2317
2378
|
}
|
|
2318
2379
|
|
|
2319
|
-
// Remove only
|
|
2380
|
+
// Remove only byte-verified Brain bridges with an exact shipped hook definition.
|
|
2320
2381
|
// Foreign commands, mixed command strings, unknown shapes, and their surrounding settings survive.
|
|
2321
2382
|
export function retireManagedHookRegistrations({ home = os.homedir(), codexDir = codexHomeDir(),
|
|
2322
2383
|
files = [path.join(home, '.claude', 'settings.json'), path.join(home, '.claude', 'settings.local.json'),
|
|
2323
2384
|
path.join(codexDir, 'hooks.json')], wrapperPath = codexHookWrapperPath(codexDir) } = {}) {
|
|
2324
2385
|
let removed = 0;
|
|
2325
2386
|
const changedFiles = [];
|
|
2326
|
-
const
|
|
2327
|
-
|
|
2387
|
+
const conflicts = [];
|
|
2388
|
+
const verifiedWrapper = legacyBrainWrapperOwnership(wrapperPath, path.dirname(codexDir)).state === 'verified';
|
|
2389
|
+
const tokensOf = (command) => (command.match(/"[^"]*"|'[^']*'|[^\s]+/g) || [])
|
|
2390
|
+
.map((token) => token.replace(/^["']|["']$/g, ''));
|
|
2391
|
+
const registry = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'plugin', 'hooks', 'codex-hooks.json'), 'utf8'));
|
|
2392
|
+
const definitions = Object.entries(registry.hooks).flatMap(([event, groups]) => groups.flatMap(group =>
|
|
2393
|
+
group.hooks.flatMap(hook => {
|
|
2394
|
+
const values = typeof hook.command === 'string' ? tokensOf(hook.command) : [];
|
|
2395
|
+
return values[0] === 'node' && values[1] === '-e' && /^\d+$/.test(values[3] || '')
|
|
2396
|
+
? [{ event, matcher: String(group.matcher ?? '*'), hook, args: values.slice(4) }] : [];
|
|
2397
|
+
})));
|
|
2398
|
+
const owns = (hook, event, group) => {
|
|
2328
2399
|
const command = hook?.command;
|
|
2329
|
-
if (typeof command !== 'string' ||
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
return
|
|
2333
|
-
|
|
2400
|
+
if (!verifiedWrapper || typeof command !== 'string' || hook.type !== 'command'
|
|
2401
|
+
|| (hook.pluginId != null && hook.pluginId !== 'ruvnet-brain@ruvnet-brain')
|
|
2402
|
+
|| Object.keys(group).some(key => !['matcher', 'hooks'].includes(key))) return false;
|
|
2403
|
+
return definitions.some(definition => {
|
|
2404
|
+
if (definition.event !== event || definition.matcher !== String(group.matcher ?? '*')) return false;
|
|
2405
|
+
if (Object.keys(hook).some(key => !['command', 'pluginId', ...Object.keys(definition.hook)].includes(key))) return false;
|
|
2406
|
+
if (Object.keys(definition.hook).some(key => key !== 'command' && hook[key] !== definition.hook[key])) return false;
|
|
2407
|
+
if (Object.keys(hook).some(key => !['command', 'pluginId'].includes(key)
|
|
2408
|
+
&& hook[key] !== definition.hook[key])) return false;
|
|
2409
|
+
if (command === definition.hook.command) return true;
|
|
2410
|
+
if (/[;&|`$\n\r]/.test(command)) return false;
|
|
2411
|
+
// Match literal installer forms, not a shell-like split: adjacent quoted tokens
|
|
2412
|
+
// can execute a different script even when a permissive splitter looks identical.
|
|
2413
|
+
if (/["']/.test(wrapperPath) || (process.platform !== 'win32' && wrapperPath.includes('\\'))
|
|
2414
|
+
|| (process.platform === 'win32' && /[%!^]/.test(command))
|
|
2415
|
+
|| definition.args.some(arg => !/^[\w-]+$/.test(arg))) return false;
|
|
2416
|
+
const scripts = [`"${wrapperPath}"`];
|
|
2417
|
+
if (process.platform !== 'win32') scripts.push(`'${wrapperPath}'`);
|
|
2418
|
+
if (/^[\w/.:~-]+$/.test(wrapperPath)) scripts.push(wrapperPath);
|
|
2419
|
+
const nodes = ['node', `"${process.execPath}"`];
|
|
2420
|
+
if (process.platform !== 'win32') nodes.push(`'${process.execPath}'`);
|
|
2421
|
+
if (/^[\w/.:~-]+$/.test(process.execPath)) nodes.push(process.execPath);
|
|
2422
|
+
return nodes.some(node => scripts.some(script =>
|
|
2423
|
+
command === `${node} ${script} ${definition.args.join(' ')}`));
|
|
2424
|
+
});
|
|
2334
2425
|
};
|
|
2335
2426
|
for (const file of files) {
|
|
2427
|
+
let stat;
|
|
2428
|
+
try { stat = fs.lstatSync(file); } catch (error) {
|
|
2429
|
+
if (error.code === 'ENOENT') continue;
|
|
2430
|
+
throw error;
|
|
2431
|
+
}
|
|
2432
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
|
|
2433
|
+
conflicts.push({ file, reason: 'shared or indirect settings preserved' });
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2336
2436
|
let before;
|
|
2337
2437
|
try { before = fs.readFileSync(file, 'utf8'); } catch (error) {
|
|
2338
2438
|
if (error.code === 'ENOENT') continue;
|
|
@@ -2345,7 +2445,16 @@ export function retireManagedHookRegistrations({ home = os.homedir(), codexDir =
|
|
|
2345
2445
|
if (!Array.isArray(groups)) continue;
|
|
2346
2446
|
document.hooks[event] = groups.flatMap((group) => {
|
|
2347
2447
|
if (!Array.isArray(group?.hooks)) return [group];
|
|
2348
|
-
const hooks = group.hooks.filter((hook) => {
|
|
2448
|
+
const hooks = group.hooks.filter((hook) => {
|
|
2449
|
+
if (!owns(hook, event, group)) {
|
|
2450
|
+
if (hook?.pluginId === 'ruvnet-brain@ruvnet-brain'
|
|
2451
|
+
|| (typeof hook?.command === 'string' && hook.command.includes(wrapperPath))) {
|
|
2452
|
+
conflicts.push({ file, event, reason: 'unverified or customized hook preserved' });
|
|
2453
|
+
}
|
|
2454
|
+
return true;
|
|
2455
|
+
}
|
|
2456
|
+
count++; return false;
|
|
2457
|
+
});
|
|
2349
2458
|
return hooks.length || group.hooks.length === 0 ? [{ ...group, hooks }] : [];
|
|
2350
2459
|
});
|
|
2351
2460
|
if (groups.length && document.hooks[event].length === 0) delete document.hooks[event];
|
|
@@ -2355,7 +2464,7 @@ export function retireManagedHookRegistrations({ home = os.homedir(), codexDir =
|
|
|
2355
2464
|
removed += count;
|
|
2356
2465
|
changedFiles.push(file);
|
|
2357
2466
|
}
|
|
2358
|
-
return { removed, changedFiles };
|
|
2467
|
+
return { removed, changedFiles, conflicts };
|
|
2359
2468
|
}
|
|
2360
2469
|
|
|
2361
2470
|
export function automaticHookRetirementStatus(root = REPO_ROOT, { scope = 'source' } = {}) {
|
|
@@ -2997,13 +3106,7 @@ async function doctorRun({ json }) {
|
|
|
2997
3106
|
hostConvergence = classifyHostConvergence(recorded);
|
|
2998
3107
|
const codex = recorded.hosts?.codex;
|
|
2999
3108
|
if (codex?.state === 'ready' && codex.restartRequired === true && codex.restartScope === 'unproven') {
|
|
3000
|
-
|
|
3001
|
-
if (!process.env.CODEX_BIN) {
|
|
3002
|
-
try {
|
|
3003
|
-
const configured = JSON.parse(fs.readFileSync(path.join(path.dirname(convergencePath), 'model-routing', 'terminal-launcher-config.json'), 'utf8'));
|
|
3004
|
-
if (typeof configured.realCodex === 'string' && path.isAbsolute(configured.realCodex)) binary = configured.realCodex;
|
|
3005
|
-
} catch { /* native PATH resolution remains bounded and fail closed */ }
|
|
3006
|
-
}
|
|
3109
|
+
const binary = codexAdministrativeProbeBinary();
|
|
3007
3110
|
const fresh = await probeFreshCodexDeclarations({ binary, codexHome: codexHomeDir(), cwd: process.cwd(),
|
|
3008
3111
|
releasedPluginRoot: path.join(REPO_ROOT, 'plugin'), expectedVersion: PACKAGE_VERSION });
|
|
3009
3112
|
hostConvergence = reconcileFreshCodexDeclarations(recorded, fresh);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.15",
|
|
4
4
|
"description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
3
|
"description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
|
|
4
|
-
"version": "4.5.
|
|
4
|
+
"version": "4.5.15",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -213,7 +213,11 @@ export function auditCompletionClaims(message, { turn = null, host = 'claude' }
|
|
|
213
213
|
if (!namesCheck) problems.push('the answer does not name the check that proves it');
|
|
214
214
|
if (!disclosesGaps) problems.push('the answer does not disclose what is NOT verified');
|
|
215
215
|
const transcriptOk = turn ? verification.checks.length > 0 : false;
|
|
216
|
-
const
|
|
216
|
+
const scopedCheck = claims.every(claim => /^(?:The\s+)?(?:targeted|selected|unit|syntax)\s+(?:unit\s+)?(?:checks?|tests?)\s+(?:is|are)\s+(?:now\s+)?(?:passing|green|verified)[.!]?$/i.test(claim.text));
|
|
217
|
+
// A transcript command is an observed check, not proof of the owner's whole task.
|
|
218
|
+
if (transcriptOk && namesCheck && disclosesGaps && !scopedCheck) problems.push('observed checks cover only their executed scope; whole-task completion is UNKNOWN');
|
|
219
|
+
const verdict = transcriptOk && namesCheck && disclosesGaps && scopedCheck ? 'OBSERVED_CHECK'
|
|
220
|
+
: namesCheck && disclosesGaps ? 'UNKNOWN' : 'FAIL';
|
|
217
221
|
return { verdict, claims, verification, namesCheck, disclosesGaps, problems };
|
|
218
222
|
}
|
|
219
223
|
|
|
@@ -272,16 +272,12 @@ if (has('--complete-objective')) {
|
|
|
272
272
|
}
|
|
273
273
|
const led = load();
|
|
274
274
|
if (led.objective) {
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
// verified fact standing in for a real completion check.
|
|
282
|
-
led.objective.state = evidence.trim() ? 'completed' : led.objective.state;
|
|
283
|
-
led.objective.completedAt = new Date().toISOString();
|
|
284
|
-
led.objective.completionEvidence = evidence;
|
|
275
|
+
// Prose is a request, never verified completion or authority to suppress truth audits.
|
|
276
|
+
led.objective.completionRequestedAt = new Date().toISOString();
|
|
277
|
+
led.objective.completionRequest = evidence.trim();
|
|
278
|
+
save(led);
|
|
279
|
+
console.error('completion unverified: prose cannot complete this objective; retain unfinished work');
|
|
280
|
+
process.exit(2);
|
|
285
281
|
}
|
|
286
282
|
save(led);
|
|
287
283
|
console.log(led.objective ? `objective completed: ${evidence}` : 'no objective to complete');
|
|
@@ -315,8 +311,8 @@ if (has('--help') || has('-h')) {
|
|
|
315
311
|
' --set-commitment-state <blocked|deferred|superseded|disputed> --item "<exact text>"',
|
|
316
312
|
' --session-id "<capturing session>" --reason "<why>" [--replacement "<reference>"]',
|
|
317
313
|
' retain a noncompleted assistant item; user work unchanged',
|
|
318
|
-
' --complete-objective "<evidence>"
|
|
319
|
-
'
|
|
314
|
+
' --complete-objective "<evidence>" request closure; prose alone never marks completed',
|
|
315
|
+
' unverified requests retain the unfinished objective;',
|
|
320
316
|
' untouched',
|
|
321
317
|
' --cancel-objective "<reason>" close the current objective as state: cancelled, with the',
|
|
322
318
|
' reason recorded — leaves other ledger items untouched',
|
|
@@ -372,7 +368,7 @@ const completion = (() => {
|
|
|
372
368
|
const turn = HOST === 'claude' ? readClaudeTurn(hookInput.transcript_path) : null;
|
|
373
369
|
// Claude always sends a JSONL transcript; one we cannot read is OUR failure, and a Stop hook
|
|
374
370
|
// fails open on its own machinery (ADR-074 failure semantics) — never a correction request.
|
|
375
|
-
|
|
371
|
+
// Missing native evidence stays UNKNOWN; it cannot silently validate a completion claim.
|
|
376
372
|
return auditCompletionClaims(message, { turn, host: HOST });
|
|
377
373
|
} catch { return { verdict: 'NONE', claims: [] }; }
|
|
378
374
|
})();
|
|
@@ -427,8 +423,7 @@ promiseBookkeeping();
|
|
|
427
423
|
* emitted on a stop that is already a continuation.
|
|
428
424
|
*/
|
|
429
425
|
if (hookInput.stop_hook_active) process.exit(EXIT_ALLOW);
|
|
430
|
-
//
|
|
431
|
-
if (['cancelled', 'completed', 'blocked'].includes(led.objective?.state)) process.exit(EXIT_ALLOW);
|
|
426
|
+
// A terminal preference suppresses only its own nudge, never independent truth audits.
|
|
432
427
|
const objective = authorizedContinuationObjective(led.objective, hookInput, projectIdentity);
|
|
433
428
|
|
|
434
429
|
// LOOP-SAFETY 1b (GPT-5.6-Sol review) — an empty-but-parseable `{}` is NOT a real Stop payload; a genuine
|
|
@@ -665,7 +660,7 @@ if (observations.length) console.error(JSON.stringify({ kind: 'continuation-advi
|
|
|
665
660
|
authority: false, items: observations.map(({ text, at }) => ({ text, at })) }));
|
|
666
661
|
// ONE correction per turn: a single item however many claims the answer made; the stop_hook_active
|
|
667
662
|
// exit and the cooldown lock below bound it to one request per stop episode.
|
|
668
|
-
const completionWork = completion.verdict
|
|
663
|
+
const completionWork = ['FAIL', 'UNKNOWN'].includes(completion.verdict) ? [{
|
|
669
664
|
text: `You claimed "${completion.claims[0].text.slice(0, 160)}" is done; ${completion.problems.join('; ')}. Run the real consumer path, or restate it as UNVERIFIED.`,
|
|
670
665
|
at: new Date(nowMs).toISOString(), derived: true, kind: 'completion-claim-integrity',
|
|
671
666
|
}] : [];
|
|
@@ -733,7 +728,9 @@ const ageLabel = (i) => {
|
|
|
733
728
|
* This replaces a read-lastForcedAt-then-write that failed OPEN on a write error and let two hooks race.
|
|
734
729
|
*/
|
|
735
730
|
const COOLDOWN_MS = Number(process.env.RUVNET_CONTINUATION_COOLDOWN_MS ?? 20000);
|
|
736
|
-
const
|
|
731
|
+
const episode = crypto.createHash('sha256').update(JSON.stringify([projectIdentity.projectId, projectIdentity.worktreeId,
|
|
732
|
+
hookInput.session_id, led.objective?.id ?? null, hookInput.turn_id ?? String(hookInput.last_assistant_message || '')])).digest('hex');
|
|
733
|
+
const LOCK = LEDGER + '.cooldown.' + episode;
|
|
737
734
|
function claimCooldown(now, windowMs) {
|
|
738
735
|
try {
|
|
739
736
|
const prev = Date.parse(fs.readFileSync(LOCK, 'utf8'));
|
|
@@ -771,8 +768,8 @@ const header = capabilityClaims.length
|
|
|
771
768
|
? ['Your proposed final answer contains a RuvNet capability claim that is contradicted or not provable.',
|
|
772
769
|
'Do NOT deliver it unchanged — continue now and correct the claim from the sealed live-host inventory.']
|
|
773
770
|
: completionClaims.length
|
|
774
|
-
? ['Your proposed final answer claims
|
|
775
|
-
'
|
|
771
|
+
? ['Your proposed final answer claims completion beyond the available verified scope.',
|
|
772
|
+
'Correct only that claim: report the observed check scope and what remains UNVERIFIED.']
|
|
776
773
|
: committed.length && observed.length
|
|
777
774
|
? [`You have unfinished work you committed to, and ${repoLabel} has open work of its own.`,
|
|
778
775
|
'Do NOT end the turn — continue now.']
|
|
@@ -836,12 +833,4 @@ const lines = [
|
|
|
836
833
|
: []),
|
|
837
834
|
];
|
|
838
835
|
|
|
839
|
-
process.stdout.write(JSON.stringify({
|
|
840
|
-
hookSpecificOutput: {
|
|
841
|
-
hookEventName: 'Stop', // must name the firing event or the envelope is discarded
|
|
842
|
-
additionalContext: lines.join('\n'),
|
|
843
|
-
},
|
|
844
|
-
}));
|
|
845
|
-
|
|
846
|
-
// Exit 0 regardless. This gate informs at the boundary; it never breaks the turn.
|
|
847
|
-
process.exit(EXIT_ALLOW);
|
|
836
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason: lines.join('\n') }));
|
|
@@ -24,7 +24,7 @@ export function authorizedContinuationObjective(objective, input, identity) {
|
|
|
24
24
|
if (!identity || input?.hook_event_name !== 'Stop' || !text(input.session_id)
|
|
25
25
|
|| input.interrupted || input.cancelled || input.stop_hook_active) return null;
|
|
26
26
|
if (objective?.schemaVersion !== 1 || objective.kind !== 'continuation-preferences'
|
|
27
|
-
|| objective.authoritative !== false || objective.state
|
|
27
|
+
|| objective.authoritative !== false || !['active', 'completed'].includes(objective.state)
|
|
28
28
|
|| !text(objective.id) || !text(objective.text) || !Number.isFinite(Date.parse(objective.at))
|
|
29
29
|
|| objective.authorization?.kind !== 'user' || !text(objective.authorization.reference)
|
|
30
30
|
|| objective.projectId !== identity.projectId
|
|
@@ -185,6 +185,16 @@ const TABLE = {
|
|
|
185
185
|
};
|
|
186
186
|
|
|
187
187
|
const hookId = process.argv[2];
|
|
188
|
+
// Native Claude supplies this exact registered plugin root; do not guess a host from absence.
|
|
189
|
+
if (['session-snapshot', 'continuation-gate'].includes(hookId)
|
|
190
|
+
&& process.env.RUVNET_HOOK_HOST === undefined && process.env.CLAUDE_PLUGIN_ROOT) {
|
|
191
|
+
try {
|
|
192
|
+
const ownPluginRoot = fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'));
|
|
193
|
+
if (fs.realpathSync(process.env.CLAUDE_PLUGIN_ROOT) === ownPluginRoot) process.env.RUVNET_HOOK_HOST = 'claude';
|
|
194
|
+
} catch { /* Missing or mismatched native registration remains unknown. */ }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
188
198
|
const entry = TABLE[hookId];
|
|
189
199
|
if (!entry) {
|
|
190
200
|
process.stderr.write(`[hook-shim] unknown hook id: ${JSON.stringify(hookId)} — known: ${Object.keys(TABLE).join(', ')}\n`);
|
|
@@ -30,19 +30,22 @@ function verifyRefs(refs) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/** Actual native bytes only; large histories use a source-bound native compaction projection. */
|
|
34
34
|
export async function captureNativeParentContext({ harness, sessionId, env = process.env,
|
|
35
35
|
evidenceRoot = path.join(env.HOME || os.homedir(), '.cache/ruvnet-brain/model-routing/parent-context'),
|
|
36
|
-
observeCodex } = {}) {
|
|
36
|
+
observeCodex, deadline = Infinity, signal } = {}) {
|
|
37
37
|
requireValue(process.platform !== 'win32', 'private native parent transcript capture unsupported on Windows; ACL proof unavailable');
|
|
38
38
|
if (!sessionId) return [];
|
|
39
39
|
requireValue(/^[a-f0-9-]{36}$/i.test(sessionId) && ['codex', 'claude-code'].includes(harness), 'native parent identity required');
|
|
40
|
-
let source, expected;
|
|
40
|
+
let source, expected, sourceBound, observedTurns, observedBytes;
|
|
41
41
|
if (harness === 'codex') {
|
|
42
42
|
const observe = observeCodex ?? (await import('./model-routing-execution-adapters.mjs')).readCodexWorkerObservation;
|
|
43
|
-
const observed = observe(sessionId, { home: env.CODEX_HOME || path.join(env.HOME || os.homedir(), '.codex'), allowHistory: true });
|
|
43
|
+
const observed = observe(sessionId, { home: env.CODEX_HOME || path.join(env.HOME || os.homedir(), '.codex'), allowHistory: true, deadline, signal });
|
|
44
44
|
requireValue(observed?.sessionId === sessionId && HASH.test(observed.evidence?.sha256 || ''), 'actual Codex parent evidence unproven');
|
|
45
|
-
source = observed.evidence.path; expected = observed.evidence.sha256;
|
|
45
|
+
source = observed.evidence.path; expected = observed.evidence.sha256; sourceBound = observed.evidence.sourceBound;
|
|
46
|
+
observedTurns = observed.turnCount; observedBytes = observed.evidence.byteLength;
|
|
47
|
+
const nativeHome = fs.realpathSync(env.CODEX_HOME || path.join(env.HOME || os.homedir(), '.codex'));
|
|
48
|
+
requireValue(source.startsWith(path.join(nativeHome, 'sessions') + path.sep), 'parent evidence escaped native session home');
|
|
46
49
|
} else {
|
|
47
50
|
const projects = path.join(env.CLAUDE_CONFIG_DIR || path.join(env.HOME || os.homedir(), '.claude'), 'projects');
|
|
48
51
|
const matches = [];
|
|
@@ -57,14 +60,54 @@ export async function captureNativeParentContext({ harness, sessionId, env = pro
|
|
|
57
60
|
requireValue(path.isAbsolute(source || '') && fs.realpathSync(source) === source, 'canonical native parent transcript required');
|
|
58
61
|
const fd = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW); let bytes;
|
|
59
62
|
try {
|
|
60
|
-
const before = fs.fstatSync(fd);
|
|
61
|
-
requireValue(before.isFile() && before.uid === process.getuid
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
const before = fs.fstatSync(fd, { bigint: true }), limit = 16 * 1024 * 1024;
|
|
64
|
+
requireValue(before.isFile() && before.uid === BigInt(process.getuid()), 'owned bounded parent transcript required');
|
|
65
|
+
const live = () => requireValue(!signal?.aborted && Date.now() < deadline, 'parent capture cancelled or expired'); live();
|
|
66
|
+
if (before.size <= BigInt(limit)) {
|
|
67
|
+
sourceBound = undefined; bytes = Buffer.alloc(Number(before.size)); let offset = 0;
|
|
68
|
+
while (offset < bytes.length) {
|
|
69
|
+
live(); const count = fs.readSync(fd, bytes, offset, Math.min(256 * 1024, bytes.length - offset), offset);
|
|
70
|
+
requireValue(count > 0, 'parent transcript truncated while captured'); offset += count;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
requireValue(harness === 'codex' && sourceBound?.kind === 'native-compaction-projection'
|
|
75
|
+
&& sourceBound.nativeSessionId === sessionId && sourceBound.sourceSha256 === expected
|
|
76
|
+
&& sourceBound.sourceBytes === Number(before.size) && sourceBound.sourceBytes === observedBytes
|
|
77
|
+
&& sourceBound.fullTurnCount === observedTurns && sourceBound.omittedHistoryPrefix === true
|
|
78
|
+
&& Number.isSafeInteger(sourceBound.fullTurnCount) && sourceBound.fullTurnCount > 0
|
|
79
|
+
&& Array.isArray(sourceBound.ranges) && sourceBound.ranges.length >= 2 && sourceBound.ranges.length <= 3,
|
|
80
|
+
'bounded native compaction provenance required');
|
|
81
|
+
let total = 0, priorEnd = 0;
|
|
82
|
+
for (const range of sourceBound.ranges) {
|
|
83
|
+
requireValue(Number.isSafeInteger(range.offset) && range.offset >= priorEnd && Number.isSafeInteger(range.bytes)
|
|
84
|
+
&& range.bytes > 0 && range.offset + range.bytes <= Number(before.size), 'unsafe native projection range');
|
|
85
|
+
priorEnd = range.offset + range.bytes; total += range.bytes;
|
|
86
|
+
}
|
|
87
|
+
requireValue(sourceBound.ranges[0].offset === 0 && priorEnd === Number(before.size) && total <= limit, 'native projection exceeded bound');
|
|
88
|
+
const hash = crypto.createHash('sha256'), buffer = Buffer.alloc(256 * 1024), parts = []; let offset = 0;
|
|
89
|
+
while (offset < Number(before.size)) {
|
|
90
|
+
live(); const count = fs.readSync(fd, buffer, 0, Math.min(buffer.length, Number(before.size) - offset), offset);
|
|
91
|
+
requireValue(count > 0, 'parent transcript truncated while captured'); hash.update(buffer.subarray(0, count));
|
|
92
|
+
for (const range of sourceBound.ranges) {
|
|
93
|
+
const start = Math.max(offset, range.offset), end = Math.min(offset + count, range.offset + range.bytes);
|
|
94
|
+
if (end > start) parts.push(Buffer.from(buffer.subarray(start - offset, end - offset)));
|
|
95
|
+
}
|
|
96
|
+
offset += count;
|
|
97
|
+
}
|
|
98
|
+
requireValue(hash.digest('hex') === expected, 'parent transcript evidence changed'); bytes = Buffer.concat(parts, total);
|
|
99
|
+
const rows = new TextDecoder('utf-8', { fatal: true }).decode(bytes).trim().split('\n').map(line => JSON.parse(line));
|
|
100
|
+
requireValue(rows[0]?.type === 'session_meta' && rows[0].payload?.id === sessionId && rows.filter(row => row.type === 'session_meta').length === 1
|
|
101
|
+
&& rows.some(row => row.type === 'compacted' && Array.isArray(row.payload?.replacement_history)), 'native projection provenance mismatch');
|
|
102
|
+
sourceBound = { kind: sourceBound.kind, nativeSessionId: sessionId, sourceSha256: expected, sourceBytes: Number(before.size),
|
|
103
|
+
fullTurnCount: sourceBound.fullTurnCount, omittedHistoryPrefix: true, ranges: sourceBound.ranges, nativeHistoryMutated: false };
|
|
104
|
+
}
|
|
105
|
+
const after = fs.fstatSync(fd, { bigint: true }), current = fs.lstatSync(source, { bigint: true });
|
|
106
|
+
requireValue(['dev', 'ino', 'size', 'mtimeNs', 'ctimeNs'].every(key => before[key] === after[key] && after[key] === current[key])
|
|
107
|
+
&& !current.isSymbolicLink(), 'parent transcript changed while captured'); live();
|
|
65
108
|
} finally { fs.closeSync(fd); }
|
|
66
109
|
const digest = sha(bytes);
|
|
67
|
-
requireValue(bytes.length > 0 && (!expected || digest === expected), 'parent transcript evidence changed');
|
|
110
|
+
requireValue(bytes.length > 0 && (!expected || sourceBound || digest === expected), 'parent transcript evidence changed');
|
|
68
111
|
if (harness === 'claude-code') {
|
|
69
112
|
const rows = bytes.toString('utf8').split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
70
113
|
requireValue(rows.some(row => row.sessionId === sessionId), 'actual Claude transcript session identity missing');
|
|
@@ -77,7 +120,7 @@ export async function captureNativeParentContext({ harness, sessionId, env = pro
|
|
|
77
120
|
const file = path.join(directory, `${sessionId}.jsonl`);
|
|
78
121
|
const output = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
|
|
79
122
|
try { fs.writeFileSync(output, bytes); fs.fsyncSync(output); } finally { fs.closeSync(output); }
|
|
80
|
-
return [{ path: file, digest }];
|
|
123
|
+
return [{ path: file, digest, ...(sourceBound ? { sourceBound } : {}) }];
|
|
81
124
|
}
|
|
82
125
|
|
|
83
126
|
async function service() {
|
|
@@ -264,7 +307,7 @@ export async function runManagedPrompt({ originalPrompt, prompt = originalPrompt
|
|
|
264
307
|
}
|
|
265
308
|
if (!contextRefs.length && (retainedContext.sessionId || retainedContext.threadId)) {
|
|
266
309
|
contextRefs = await bounded(() => captureContext({ harness, sessionId: retainedContext.sessionId ?? retainedContext.threadId,
|
|
267
|
-
env: primaryOptions.env ?? process.env }));
|
|
310
|
+
env: primaryOptions.env ?? process.env, deadline, signal: combined }));
|
|
268
311
|
requireValue(contextRefs.length > 0, 'existing parent transcript required');
|
|
269
312
|
}
|
|
270
313
|
requireValue(original.length <= 120000, 'substantive request exceeds bounded completion-frame input');
|
|
@@ -18,36 +18,93 @@ export function nativeWorkflowBinaries(home = os.homedir()) {
|
|
|
18
18
|
return { claude: config.realClaude, codex: config.realCodex };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function readCodexWorkerObservation(sessionId, { home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), now = Date.now(), expectedPriorTurns, evidencePath, allowHistory = false } = {}) {
|
|
21
|
+
export function readCodexWorkerObservation(sessionId, { home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), now = Date.now(), expectedPriorTurns, evidencePath, expectedPrefix, allowHistory = false, deadline = Infinity, signal } = {}) {
|
|
22
22
|
if (!/^[a-f0-9-]{36}$/.test(sessionId || '')) throw blocked('Native worker session identity missing');
|
|
23
23
|
const matches = [];
|
|
24
24
|
const directories = [];
|
|
25
|
-
const base = path.join(home, 'sessions');
|
|
26
|
-
if (evidencePath)
|
|
25
|
+
const base = path.join(fs.existsSync(home) ? fs.realpathSync(home) : home, 'sessions');
|
|
26
|
+
if (evidencePath) {
|
|
27
|
+
const directory = fs.realpathSync(path.dirname(evidencePath));
|
|
28
|
+
if (!directory.startsWith(base + path.sep)) throw blocked('Native evidence escaped its session home');
|
|
29
|
+
evidencePath = path.join(directory, path.basename(evidencePath)); directories.push(directory);
|
|
30
|
+
}
|
|
27
31
|
else if ((expectedPriorTurns !== undefined || allowHistory) && fs.existsSync(base)) {
|
|
28
32
|
for (const year of fs.readdirSync(base)) if (/^\d{4}$/.test(year))
|
|
29
33
|
for (const month of fs.readdirSync(path.join(base, year))) if (/^\d{2}$/.test(month))
|
|
30
34
|
for (const day of fs.readdirSync(path.join(base, year, month))) if (/^\d{2}$/.test(day)) directories.push(path.join(base, year, month, day));
|
|
31
35
|
} else for (const offset of [0, 86400000]) {
|
|
32
36
|
const date = new Date(now - offset).toISOString().slice(0, 10).split('-');
|
|
33
|
-
directories.push(path.join(
|
|
37
|
+
directories.push(path.join(base, ...date));
|
|
34
38
|
}
|
|
35
39
|
for (const directory of new Set(directories)) {
|
|
36
40
|
if (!fs.existsSync(directory)) continue;
|
|
37
41
|
for (const name of fs.readdirSync(directory)) {
|
|
38
42
|
if (!name.endsWith(`-${sessionId}.jsonl`)) continue;
|
|
39
43
|
const file = path.join(directory, name), stat = fs.lstatSync(file);
|
|
40
|
-
if (!stat.isFile() || stat.isSymbolicLink() ||
|
|
41
|
-
matches.push({ file
|
|
44
|
+
if (!stat.isFile() || stat.isSymbolicLink() || fs.realpathSync(file) !== file || process.getuid && stat.uid !== process.getuid()) throw blocked('Unsafe native worker evidence file');
|
|
45
|
+
if (!evidencePath || evidencePath === file) matches.push({ file });
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
if (matches.length !== 1) throw blocked('Exact native worker rollout unavailable or ambiguous');
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
const file = matches[0].file, fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
50
|
+
const limit = 16 * 1024 * 1024, decoder = new TextDecoder('utf-8', { fatal: true }), hash = crypto.createHash('sha256');
|
|
51
|
+
const prefix = expectedPrefix && crypto.createHash('sha256'), buffer = Buffer.alloc(256 * 1024);
|
|
52
|
+
let before, offset = 0, rowOffset = 0, rowBytes = 0, parts = [], context, contextRange, meta, compact, turnCount = 0, metaCount = 0;
|
|
53
|
+
const live = () => { if (signal?.aborted || Date.now() >= deadline) throw blocked('Native evidence capture cancelled or expired'); };
|
|
54
|
+
const consume = () => {
|
|
55
|
+
let row;
|
|
56
|
+
try { row = JSON.parse(decoder.decode(Buffer.concat(parts, rowBytes))); } catch { throw blocked('Malformed native evidence record or UTF-8'); }
|
|
57
|
+
if (!row || typeof row !== 'object' || typeof row.type !== 'string') throw blocked('Malformed native evidence record');
|
|
58
|
+
const range = { offset: rowOffset, bytes: rowBytes };
|
|
59
|
+
if (row.type === 'session_meta') { metaCount++; meta = { ...range, id: row.payload?.id }; }
|
|
60
|
+
if (row.type === 'turn_context') { turnCount++; context = row.payload; contextRange = range; }
|
|
61
|
+
if (row.type === 'compacted') {
|
|
62
|
+
if (!Array.isArray(row.payload?.replacement_history) || !row.payload.replacement_history.length) throw blocked('Native compaction provenance unavailable');
|
|
63
|
+
compact = range;
|
|
64
|
+
}
|
|
65
|
+
rowOffset += rowBytes; rowBytes = 0; parts = [];
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
before = fs.fstatSync(fd, { bigint: true });
|
|
69
|
+
if (!before.isFile() || process.getuid && before.uid !== BigInt(process.getuid()) || before.size > BigInt(Number.MAX_SAFE_INTEGER)
|
|
70
|
+
|| !process.getuid && before.size > BigInt(limit)) throw blocked('Unsafe native worker evidence file');
|
|
71
|
+
if (expectedPrefix && (!Number.isSafeInteger(expectedPrefix.bytes) || expectedPrefix.bytes < 0 || expectedPrefix.bytes > Number(before.size)
|
|
72
|
+
|| !/^[a-f0-9]{64}$/.test(expectedPrefix.sha256 || ''))) throw blocked('Native history prefix changed');
|
|
73
|
+
while (offset < Number(before.size)) {
|
|
74
|
+
live(); const count = fs.readSync(fd, buffer, 0, Math.min(buffer.length, Number(before.size) - offset), offset);
|
|
75
|
+
if (!count) throw blocked('Native evidence truncated during capture');
|
|
76
|
+
hash.update(buffer.subarray(0, count));
|
|
77
|
+
if (prefix && offset < expectedPrefix.bytes) prefix.update(buffer.subarray(0, Math.min(count, expectedPrefix.bytes - offset)));
|
|
78
|
+
let start = 0;
|
|
79
|
+
while (start < count) {
|
|
80
|
+
const newline = buffer.indexOf(10, start), end = newline >= start && newline < count ? newline + 1 : count;
|
|
81
|
+
const part = Buffer.from(buffer.subarray(start, end)); rowBytes += part.length;
|
|
82
|
+
if (rowBytes > limit) throw blocked('Native evidence record exceeded bound');
|
|
83
|
+
parts.push(part); if (end <= count && buffer[end - 1] === 10) consume(); start = end;
|
|
84
|
+
}
|
|
85
|
+
offset += count;
|
|
86
|
+
}
|
|
87
|
+
if (rowBytes) { if (Number(before.size) > limit) throw blocked('Partial native evidence terminal record'); consume(); }
|
|
88
|
+
const after = fs.fstatSync(fd, { bigint: true }), current = fs.lstatSync(file, { bigint: true });
|
|
89
|
+
if (['dev', 'ino', 'size', 'mtimeNs', 'ctimeNs'].some(key => before[key] !== after[key] || after[key] !== current[key]) || current.isSymbolicLink()) throw blocked('Native evidence changed during capture');
|
|
90
|
+
live();
|
|
91
|
+
} finally { fs.closeSync(fd); }
|
|
92
|
+
if (prefix && prefix.digest('hex') !== expectedPrefix.sha256) throw blocked('Native history prefix changed');
|
|
93
|
+
if (!turnCount || !context || !allowHistory && turnCount !== (expectedPriorTurns === undefined ? 1 : expectedPriorTurns + 1)) throw blocked('Native worker has unexpected turn history');
|
|
94
|
+
const sha256 = hash.digest('hex'), bytes = Number(before.size);
|
|
95
|
+
if (metaCount > 1 || meta && meta.id !== sessionId) throw blocked('Native history session provenance mismatch');
|
|
96
|
+
let sourceBound;
|
|
97
|
+
if (bytes > limit) {
|
|
98
|
+
if (metaCount !== 1 || meta?.id !== sessionId) throw blocked('Native history session provenance mismatch');
|
|
99
|
+
if (compact) {
|
|
100
|
+
const ranges = [{ offset: meta.offset, bytes: meta.bytes }, ...(contextRange.offset < compact.offset ? [contextRange] : []), { offset: compact.offset, bytes: bytes - compact.offset }];
|
|
101
|
+
if (ranges.some((range, index) => index && range.offset < ranges[index - 1].offset + ranges[index - 1].bytes)) throw blocked('Native projection provenance overlaps');
|
|
102
|
+
sourceBound = { kind: 'native-compaction-projection', nativeSessionId: sessionId, sourceSha256: sha256, sourceBytes: bytes,
|
|
103
|
+
fullTurnCount: turnCount, omittedHistoryPrefix: true, ranges };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
49
106
|
return { model: context.model, effort: context.effort, cwd: context.cwd, sandbox: context.sandbox_policy, sessionId,
|
|
50
|
-
turnCount
|
|
107
|
+
turnCount, evidence: { path: file, sha256, type: 'native-turn-context', byteLength: bytes, ...(sourceBound ? { sourceBound } : {}) } };
|
|
51
108
|
}
|
|
52
109
|
|
|
53
110
|
/** Only the installed user-owned Brain search server is projected into isolated native workers. */
|
|
@@ -96,7 +153,8 @@ export async function executeCodexWorkflowWorker({ binary, decision, prompt, cwd
|
|
|
96
153
|
if (performance.now() >= limit || signal?.aborted) throw blocked('Native worker deadline expired during readiness');
|
|
97
154
|
const spec = buildLaunch(decision, { cwd });
|
|
98
155
|
const evidenceHome = clean.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
99
|
-
const prior = sessionId ? observe(sessionId, { home: evidenceHome, expectedPriorTurns: undefined, allowHistory: true
|
|
156
|
+
const prior = sessionId ? observe(sessionId, { home: evidenceHome, expectedPriorTurns: undefined, allowHistory: true,
|
|
157
|
+
deadline: Date.now() + Math.max(0, limit - performance.now()), signal }) : null;
|
|
100
158
|
const args = sessionId ? ['exec', 'resume', '--ignore-user-config', '--skip-git-repo-check', '--json', '--model', decision.model,
|
|
101
159
|
'-c', `model_reasoning_effort=\"${decision.effort}\"`, '-c', 'model_provider=\"openai\"',
|
|
102
160
|
'-c', 'service_tier=\"default\"', '-c', `sandbox_mode=\"${readOnly ? 'read-only' : 'workspace-write'}\"`,
|
|
@@ -104,6 +162,7 @@ export async function executeCodexWorkflowWorker({ binary, decision, prompt, cwd
|
|
|
104
162
|
: [...spec.args.slice(0, -1), '--skip-git-repo-check', '--json', '--sandbox', readOnly ? 'read-only' : 'workspace-write',
|
|
105
163
|
'-c', 'features.multi_agent=false', '-c', 'features.multi_agent_v2=false', '-'];
|
|
106
164
|
args.splice(args.length - 1, 0, ...codexBrainSearchArguments(clean));
|
|
165
|
+
if (performance.now() >= limit || signal?.aborted) throw blocked('Native worker deadline expired before launch');
|
|
107
166
|
const result = await new Promise((resolve, reject) => {
|
|
108
167
|
const child = launch(binary, args, { cwd, env: clean, shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
109
168
|
let stdout = '', stderr = '', finished = false, timer, killTimer, cancelled = false, cancelReason;
|
|
@@ -140,7 +199,9 @@ export async function executeCodexWorkflowWorker({ binary, decision, prompt, cwd
|
|
|
140
199
|
throw blocked('Native Codex turn did not complete successfully');
|
|
141
200
|
}
|
|
142
201
|
if (sessionId && thread?.thread_id !== sessionId) throw blocked('Native parent session changed');
|
|
143
|
-
const observation = observe(thread?.thread_id,
|
|
202
|
+
const observation = observe(thread?.thread_id, { home: evidenceHome, deadline: Date.now() + Math.max(0, limit - performance.now()), signal,
|
|
203
|
+
...(sessionId ? { expectedPriorTurns: prior.turnCount, evidencePath: prior.evidence.path,
|
|
204
|
+
expectedPrefix: { bytes: prior.evidence.byteLength, sha256: prior.evidence.sha256 } } : {}) });
|
|
144
205
|
if (observation.model !== decision.model || observation.effort !== decision.effort || observation.cwd !== cwd
|
|
145
206
|
|| (readOnly && observation.sandbox?.type !== 'read-only')) throw blocked('Native worker model, effort, directory or sandbox mismatch');
|
|
146
207
|
const messages = events.filter(event => event.type === 'item.completed' && event.item?.type === 'agent_message');
|
|
@@ -155,8 +155,8 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
155
155
|
"tests/unit/model-routing-defence.test.mjs"
|
|
156
156
|
],
|
|
157
157
|
"platformFiles": {
|
|
158
|
-
"linux": ["tests/unit/model-terminal-canonical-entry.test.mjs", "tests/unit/codex-managed-terminal.test.mjs", "tests/unit/grok-subscription-host.test.mjs", "tests/unit/model-managed-parent-context-posix.test.mjs"],
|
|
159
|
-
"macos": ["tests/unit/model-terminal-canonical-entry.test.mjs", "tests/unit/codex-managed-terminal.test.mjs", "tests/unit/grok-subscription-host.test.mjs", "tests/unit/model-managed-parent-context-posix.test.mjs"]
|
|
158
|
+
"linux": ["tests/unit/model-terminal-canonical-entry.test.mjs", "tests/unit/codex-managed-terminal.test.mjs", "tests/unit/grok-subscription-host.test.mjs", "tests/unit/model-managed-parent-context-posix.test.mjs", "tests/unit/automatic-hook-retirement-posix.test.mjs"],
|
|
159
|
+
"macos": ["tests/unit/model-terminal-canonical-entry.test.mjs", "tests/unit/codex-managed-terminal.test.mjs", "tests/unit/grok-subscription-host.test.mjs", "tests/unit/model-managed-parent-context-posix.test.mjs", "tests/unit/automatic-hook-retirement-posix.test.mjs"]
|
|
160
160
|
}
|
|
161
161
|
},
|
|
162
162
|
{
|
|
@@ -243,14 +243,25 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
243
243
|
]
|
|
244
244
|
},
|
|
245
245
|
{
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
246
|
+
"id": "owner-capture-and-off-controls",
|
|
247
|
+
"reason": "Brain OFF suppresses capacity execution; promise opt-out suppresses only new capture and preserves existing closure and capability truth",
|
|
248
|
+
"files": [
|
|
249
|
+
"tests/unit/capacity-aware-parallel-work.test.mjs",
|
|
250
|
+
"tests/unit/continuation-gate-capability-truth.test.mjs",
|
|
251
|
+
"tests/unit/continuation-gate-completion-claims.test.mjs",
|
|
252
|
+
"tests/unit/continuation-gate-objective-close.test.mjs",
|
|
253
|
+
"tests/unit/continuation-objective.test.mjs",
|
|
254
|
+
"tests/unit/continuation-gate.test.mjs"
|
|
255
|
+
],
|
|
256
|
+
"platformFiles": {
|
|
257
|
+
"linux": [
|
|
258
|
+
"tests/unit/hook-shim.test.mjs"
|
|
259
|
+
],
|
|
260
|
+
"macos": [
|
|
261
|
+
"tests/unit/hook-shim.test.mjs"
|
|
262
|
+
]
|
|
263
|
+
}
|
|
264
|
+
},
|
|
254
265
|
{
|
|
255
266
|
"id": "active-managed-generation",
|
|
256
267
|
"reason": "Active code selection binds help authorization and execution to one immutable leased generation across promotion",
|