ruvnet-brain 4.5.4 → 4.5.5
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 +2 -2
- package/bin/install.mjs +144 -27
- package/config/model-router/catalog.template.json +126 -52
- package/config/model-router/policy.default.mjs +94 -75
- package/config/model-router/qualification-contract.json +124 -0
- package/config/model-router/routing-eval-cases.json +275 -0
- package/config/model-router/routing-policy.template.json +76 -0
- package/config/model-router/weekly-analyst-instruction.md +60 -0
- package/data/model-catalog.json +44 -49
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/codex-hook-adapter.mjs +18 -9
- package/scripts/codex-hook-trust-reconcile.mjs +247 -0
- package/scripts/codex-routed.sh +3 -36
- package/scripts/goldie-weekly.sh +8 -64
- package/scripts/metaharness-router.mjs +7 -1
- package/scripts/model-analyst-sandbox.mjs +54 -0
- package/scripts/model-currency-evidence.mjs +139 -0
- package/scripts/model-currency.mjs +230 -0
- package/scripts/model-native-catalog.mjs +111 -0
- package/scripts/model-native-qualification.mjs +251 -0
- package/scripts/model-router-agent-hook.mjs +136 -0
- package/scripts/model-router-dispatch.mjs +161 -0
- package/scripts/model-router-engine.mjs +155 -104
- package/scripts/model-routing-eval.mjs +108 -0
- package/scripts/model-routing-gateway.mjs +420 -0
- package/scripts/model-routing-launchers.mjs +174 -0
- package/scripts/model-routing-policy-promotion.mjs +203 -0
- package/scripts/model-weekly-analyst.mjs +299 -0
- package/scripts/model-weekly-assessment.mjs +91 -0
- package/scripts/model-weekly-cycle.mjs +183 -0
- package/scripts/model-weekly-qualification.mjs +362 -0
- package/scripts/native-subscription-usage.mjs +57 -0
- package/scripts/release-qualification-contract.mjs +41 -0
- package/scripts/security-guidance-codex-compat.mjs +142 -0
- package/scripts/user-model-prompt-hook.mjs +69 -0
|
@@ -47,6 +47,39 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
47
47
|
"tests/unit/forge-update-apply-rollback.test.mjs"
|
|
48
48
|
]
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
"id": "approved-native-routing",
|
|
52
|
+
"reason": "Current owner allocations, classification floors, native model and effort support, subscription auth and ordinary allowance gate actual stdin dispatch without expanding spend",
|
|
53
|
+
"files": [
|
|
54
|
+
"tests/unit/model-router-engine.test.mjs",
|
|
55
|
+
"tests/unit/model-router-enforcement.test.mjs",
|
|
56
|
+
"tests/unit/native-subscription-usage.test.mjs",
|
|
57
|
+
"tests/unit/model-router-agent-hook.test.mjs"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "same-session-native-transport",
|
|
62
|
+
"reason": "Codex and Claude native JSONL preserve context, UTF-8, control progress, cancellation and deferred FIFO while binding each new turn to an approved native route",
|
|
63
|
+
"files": [
|
|
64
|
+
"tests/unit/model-routing-gateway.test.mjs",
|
|
65
|
+
"tests/unit/model-routing-launchers.test.mjs"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "weekly-routing-evidence",
|
|
70
|
+
"reason": "Weekly native dispatch requires allowance and trusted tool denial; bounded completions, source fencing and qualified promotion preserve original owner approval and reject requested-only identity",
|
|
71
|
+
"files": [
|
|
72
|
+
"tests/unit/model-weekly-assessment.test.mjs",
|
|
73
|
+
"tests/unit/model-weekly-analyst.test.mjs",
|
|
74
|
+
"tests/unit/model-weekly-cycle.test.mjs",
|
|
75
|
+
"tests/unit/model-weekly-qualification.test.mjs",
|
|
76
|
+
"tests/unit/model-native-qualification.test.mjs",
|
|
77
|
+
"tests/unit/model-native-catalog.test.mjs",
|
|
78
|
+
"tests/unit/user-model-prompt-hook.test.mjs",
|
|
79
|
+
"tests/unit/model-routing-policy-promotion.test.mjs",
|
|
80
|
+
"tests/unit/codex-hook-trust.test.mjs"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
50
83
|
{
|
|
51
84
|
"id": "native-scheduler",
|
|
52
85
|
"reason": "Owned scheduler lifecycle, execution identity, measured no-op and signed installed coverage",
|
|
@@ -135,6 +168,14 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
135
168
|
"tests/integration/build-bundle-fence.test.mjs"
|
|
136
169
|
]
|
|
137
170
|
},
|
|
171
|
+
{
|
|
172
|
+
"id": "installed-routing-and-hook-trust",
|
|
173
|
+
"reason": "Actual installer update converges managed routing while preserving private overrides; POSIX archive and native metadata subprocess probes bind authorized hook trust to verified released bytes and preserve concurrent or disabled owner state",
|
|
174
|
+
"files": [
|
|
175
|
+
"tests/unit/model-router-update-convergence.test.mjs",
|
|
176
|
+
"tests/unit/codex-hook-trust-reconcile.test.mjs"
|
|
177
|
+
]
|
|
178
|
+
},
|
|
138
179
|
{
|
|
139
180
|
"id": "native-codex-discovery",
|
|
140
181
|
"reason": "Actual Codex plugin discovery and repair preserve explicit disabled state",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Explicit local repair for the Codex cache copy of security-guidance 2.0.9.
|
|
4
|
+
* Dry run by default; --apply is required. No plugin trust/settings changes.
|
|
5
|
+
* Codex 0.160.0's installed post-tool-use.command.output rejects metrics and
|
|
6
|
+
* rewakeSummary; its parser also rejects updatedMCPToolOutput semantically.
|
|
7
|
+
* This source-bound patch routes telemetry to the plugin's existing debug log,
|
|
8
|
+
* preserves security controls/context, and leaves other events/review logic intact.
|
|
9
|
+
* Future versions or different source bytes require a new reviewed compatibility
|
|
10
|
+
* patch. A successful repair is not native execution evidence or future-update support.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import crypto from 'node:crypto';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
export const COMPATIBILITY = Object.freeze({
|
|
19
|
+
version: '2.0.9', nativeContract: 'Codex 0.160.0 PostToolUse',
|
|
20
|
+
originalSha256: '2766399e43bd923ed786f8bb0fcbf6d2055b3ad4ef79ec73f722be19122de1f8',
|
|
21
|
+
patchedSha256: '4bba541507b54b6b1f882ac13d49be9b8ea865b4e3dc3fd870820a3b48749de1',
|
|
22
|
+
patchSha256: '49f0c628775d9b12a5b50ea5d3072a031c3a2e986fee050dd80f46c044677a35',
|
|
23
|
+
});
|
|
24
|
+
const PATCH = fileURLToPath(new URL('../config/hook-compat/security-guidance-2.0.9.patch', import.meta.url));
|
|
25
|
+
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
26
|
+
const noFollow = fs.constants.O_NOFOLLOW;
|
|
27
|
+
|
|
28
|
+
function requireRegularPath(file) {
|
|
29
|
+
let current = path.parse(file).root;
|
|
30
|
+
for (const segment of file.slice(current.length).split(path.sep)) {
|
|
31
|
+
current = path.join(current, segment);
|
|
32
|
+
const stat = fs.lstatSync(current);
|
|
33
|
+
if (stat.isSymbolicLink()) throw new Error(`Refusing symlink: ${current}`);
|
|
34
|
+
if (current !== file && !stat.isDirectory()) throw new Error(`Not a directory: ${current}`);
|
|
35
|
+
if (current === file && !stat.isFile()) throw new Error(`Not a regular file: ${file}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readRegular(file) {
|
|
40
|
+
requireRegularPath(file);
|
|
41
|
+
if (noFollow === undefined) throw new Error('O_NOFOLLOW is required for this repair');
|
|
42
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.fstatSync(fd);
|
|
45
|
+
if (!stat.isFile()) throw new Error(`Not a regular file: ${file}`);
|
|
46
|
+
return { bytes: fs.readFileSync(fd), stat };
|
|
47
|
+
} finally { fs.closeSync(fd); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Apply only the checked-in exact-context hunks, without invoking patch/git/Python. */
|
|
51
|
+
export function applyCompatibilityPatch(original) {
|
|
52
|
+
const patch = readRegular(PATCH).bytes;
|
|
53
|
+
if (sha256(patch) !== COMPATIBILITY.patchSha256) throw new Error('Compatibility patch digest mismatch');
|
|
54
|
+
if (sha256(original) !== COMPATIBILITY.originalSha256) throw new Error('Original source digest mismatch');
|
|
55
|
+
const source = original.toString('utf8').split('\n');
|
|
56
|
+
const lines = patch.toString('utf8').split('\n');
|
|
57
|
+
const result = [];
|
|
58
|
+
let cursor = 0;
|
|
59
|
+
for (let i = 2; i < lines.length; i++) {
|
|
60
|
+
const header = /^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/.exec(lines[i]);
|
|
61
|
+
if (!header) { if (!lines[i]) continue; throw new Error('Unexpected patch structure'); }
|
|
62
|
+
const start = Number(header[1]) - 1;
|
|
63
|
+
if (start < cursor) throw new Error('Overlapping patch hunks');
|
|
64
|
+
result.push(...source.slice(cursor, start)); cursor = start;
|
|
65
|
+
let oldCount = 0; let newCount = 0;
|
|
66
|
+
while (i + 1 < lines.length && !lines[i + 1].startsWith('@@ ') && lines[i + 1] !== '') {
|
|
67
|
+
const line = lines[++i]; const type = line[0]; const content = line.slice(1);
|
|
68
|
+
if (![' ', '-', '+'].includes(type)) throw new Error('Unsupported patch line');
|
|
69
|
+
if (type !== '+') {
|
|
70
|
+
if (source[cursor++] !== content) throw new Error('Compatibility patch context mismatch');
|
|
71
|
+
oldCount++;
|
|
72
|
+
}
|
|
73
|
+
if (type !== '-') { result.push(content); newCount++; }
|
|
74
|
+
}
|
|
75
|
+
if (oldCount !== Number(header[2]) || newCount !== Number(header[4])) throw new Error('Patch hunk count mismatch');
|
|
76
|
+
}
|
|
77
|
+
result.push(...source.slice(cursor));
|
|
78
|
+
const patched = Buffer.from(result.join('\n'));
|
|
79
|
+
if (sha256(patched) !== COMPATIBILITY.patchedSha256) throw new Error('Patched source digest mismatch');
|
|
80
|
+
return patched;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function syncDirectory(dir) {
|
|
84
|
+
const fd = fs.openSync(dir, fs.constants.O_RDONLY | noFollow);
|
|
85
|
+
try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function repairSecurityGuidance({ codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), apply = false } = {}) {
|
|
89
|
+
if (!path.isAbsolute(codexHome) || path.normalize(codexHome).split(path.sep).includes('.claude')) {
|
|
90
|
+
throw new Error('An absolute Codex home is required; Claude cache repair is forbidden');
|
|
91
|
+
}
|
|
92
|
+
const target = path.join(codexHome, 'plugins/cache/claude-plugins-official/security-guidance', COMPATIBILITY.version, 'hooks/security_reminder_hook.py');
|
|
93
|
+
const original = readRegular(target);
|
|
94
|
+
const digest = sha256(original.bytes);
|
|
95
|
+
const receipt = { target, compatibility: COMPATIBILITY, sourceSha256: digest, apply, status: 'dry-run' };
|
|
96
|
+
if (digest === COMPATIBILITY.patchedSha256) return { ...receipt, status: 'already-patched' };
|
|
97
|
+
if (digest !== COMPATIBILITY.originalSha256) throw new Error(`Unknown security-guidance source: ${digest}; refusing to patch ${target}`);
|
|
98
|
+
const patched = applyCompatibilityPatch(original.bytes);
|
|
99
|
+
if (!apply) return receipt;
|
|
100
|
+
const dir = path.dirname(target);
|
|
101
|
+
const backup = `${target}.${COMPATIBILITY.originalSha256}.original`;
|
|
102
|
+
let fd;
|
|
103
|
+
try {
|
|
104
|
+
fd = fs.openSync(backup, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
|
|
105
|
+
fs.writeFileSync(fd, original.bytes);
|
|
106
|
+
fs.fchmodSync(fd, (original.stat.mode & 0o444) || 0o400);
|
|
107
|
+
fs.fsyncSync(fd);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error.code !== 'EEXIST') throw error;
|
|
110
|
+
if (sha256(readRegular(backup).bytes) !== digest) throw new Error('Existing immutable backup digest mismatch');
|
|
111
|
+
} finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
112
|
+
syncDirectory(dir);
|
|
113
|
+
const temporary = path.join(dir, `.security-guidance-compat-${crypto.randomUUID()}.tmp`);
|
|
114
|
+
try {
|
|
115
|
+
fd = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
|
|
116
|
+
try {
|
|
117
|
+
fs.writeFileSync(fd, patched);
|
|
118
|
+
fs.fchmodSync(fd, original.stat.mode & 0o777);
|
|
119
|
+
fs.fsyncSync(fd);
|
|
120
|
+
} finally { fs.closeSync(fd); }
|
|
121
|
+
const current = readRegular(target);
|
|
122
|
+
if (current.stat.dev !== original.stat.dev || current.stat.ino !== original.stat.ino || sha256(current.bytes) !== digest) {
|
|
123
|
+
throw new Error('Plugin source changed during repair; refusing replacement');
|
|
124
|
+
}
|
|
125
|
+
fs.renameSync(temporary, target);
|
|
126
|
+
syncDirectory(dir);
|
|
127
|
+
} finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
|
|
128
|
+
return { ...receipt, status: 'patched', backup, patchedSha256: sha256(readRegular(target).bytes) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
132
|
+
try {
|
|
133
|
+
const args = process.argv.slice(2);
|
|
134
|
+
let codexHome; let apply = false;
|
|
135
|
+
for (let i = 0; i < args.length; i++) {
|
|
136
|
+
if (args[i] === '--apply') apply = true;
|
|
137
|
+
else if (args[i] === '--codex-home' && args[i + 1]) codexHome = args[++i];
|
|
138
|
+
else throw new Error(`Unknown argument: ${args[i]}`);
|
|
139
|
+
}
|
|
140
|
+
console.log(JSON.stringify(repairSecurityGuidance({ codexHome, apply }), null, 2));
|
|
141
|
+
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
142
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Per-user prompt guidance. Execution is enforced separately by the managed dispatcher.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { maybeLaunchWeeklyCycle } from './model-weekly-cycle.mjs';
|
|
9
|
+
|
|
10
|
+
export const PRESENTATION = 'For terminal briefings: give concise executive status; use narrow padded ASCII tables with borders, plain cell text, aligned columns and short rows. Never send pipe-delimited Markdown tables to this terminal. Use short lists when a table would wrap. Do not flood the response with commands or technical logs.';
|
|
11
|
+
|
|
12
|
+
export function promptContext(payload, { routerDir = path.join(os.homedir(), '.claude/model-router'), run = spawnSync, now = Date.now(), cycle = maybeLaunchWeeklyCycle } = {}) {
|
|
13
|
+
const lines = [PRESENTATION];
|
|
14
|
+
const weekly = cycle({ routerDir, now });
|
|
15
|
+
if (weekly.status === 'current' && weekly.reviewRequired === false) lines.push(`Weekly model-release check current (${weekly.checkedAt}); retain the approved policy. Full assessment runs only for newly discovered OpenAI or Anthropic models. This is not a semantic-review or promotion claim.`);
|
|
16
|
+
else if (weekly.launched) lines.push(weekly.reviewRequired
|
|
17
|
+
? 'A new model release requires assessment; the bounded review is pending and the approved policy remains active.'
|
|
18
|
+
: 'A lightweight weekly model-release check started; no full assessment is required unless a new relevant model is found.');
|
|
19
|
+
else if (weekly.status === 'blocked') lines.push(`Weekly model-release check needs attention: ${weekly.reason || 'verification unavailable'}. Retain the approved policy; do not claim no changes or a completed review.`);
|
|
20
|
+
const harness = payload?.host === 'claude-code' ? 'claude-code' : 'codex';
|
|
21
|
+
const prompt = typeof payload?.prompt === 'string' ? payload.prompt : '';
|
|
22
|
+
let policy;
|
|
23
|
+
try { policy = JSON.parse(fs.readFileSync(path.join(routerDir, 'routing-policy.json'), 'utf8')); } catch { /* report absence below */ }
|
|
24
|
+
const reviewed = Date.parse(policy?.reviewedAt);
|
|
25
|
+
const age = now - reviewed;
|
|
26
|
+
const configuredMaxAge = policy?.maxAgeMs ?? 604800000;
|
|
27
|
+
const maxAge = Math.min(configuredMaxAge, 604800000);
|
|
28
|
+
const current = policy?.schemaVersion === 1 && Number.isSafeInteger(configuredMaxAge) && configuredMaxAge > 0 && Number.isFinite(reviewed) && age >= 0;
|
|
29
|
+
if (current && age > maxAge) lines.push('Policy approval is older than seven days. Retain the owner-approved allocation and its original date; verify native availability before every launch. Age alone does not trigger reassessment.');
|
|
30
|
+
lines.push(current ? `Model routing policy reviewed ${policy.reviewedAt}; consult this user's policy for every delegated launch.` : 'Model routing policy is missing or invalid. Establish a valid approved policy before managed dispatch.');
|
|
31
|
+
if (current && prompt && prompt.length <= 65536) {
|
|
32
|
+
const engine = path.join(routerDir, 'bin/model-router-engine.mjs');
|
|
33
|
+
const result = run(process.execPath, [engine, '--harness', harness, '--policy-only', '--json'], {
|
|
34
|
+
input: prompt, encoding: 'utf8', timeout: 1500, maxBuffer: 16384,
|
|
35
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
const route = JSON.parse(result.stdout || '');
|
|
39
|
+
if (result.status === 0 && /^[a-zA-Z0-9._-]+$/.test(route.model || '') && ['low', 'medium', 'high', 'xhigh', 'max'].includes(route.effort)) {
|
|
40
|
+
lines.push(`This prompt's managed route: ${route.model}, effort ${route.effort}. Use the managed dispatcher for actual launch enforcement. This hook supplies context; it does not switch the active parent model.`);
|
|
41
|
+
} else lines.push('Prompt routing was not verified; do not infer an automatic model switch.');
|
|
42
|
+
} catch { lines.push('Prompt routing was not verified; do not infer an automatic model switch.'); }
|
|
43
|
+
}
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function envelope(context) {
|
|
48
|
+
return { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context } };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function main() {
|
|
52
|
+
let input = '';
|
|
53
|
+
const timer = setTimeout(() => process.stdin.destroy(), 1000);
|
|
54
|
+
try {
|
|
55
|
+
for await (const chunk of process.stdin) {
|
|
56
|
+
input += chunk;
|
|
57
|
+
if (input.length > 65536) { input = ''; break; }
|
|
58
|
+
}
|
|
59
|
+
} catch { /* fail open with presentation guidance */ }
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
let payload = {};
|
|
62
|
+
try { payload = JSON.parse(input); } catch { /* malformed input never launches a model */ }
|
|
63
|
+
if (process.argv.includes('--claude')) payload.host = 'claude-code';
|
|
64
|
+
process.stdout.write(JSON.stringify(envelope(promptContext(payload))) + '\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
68
|
+
main().catch(() => process.stdout.write(JSON.stringify(envelope(PRESENTATION)) + '\n'));
|
|
69
|
+
}
|