ruvnet-brain 4.5.4 → 4.5.6
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 +162 -27
- package/config/model-router/catalog.template.json +126 -52
- package/config/model-router/claude-terminal-mod/.claude-plugin/plugin.json +1 -0
- package/config/model-router/claude-terminal-mod/README.md +22 -0
- package/config/model-router/claude-terminal-mod/hooks/hooks.json +1 -0
- package/config/model-router/claude-terminal-mod/hooks/policy.default.mjs +2 -0
- package/config/model-router/claude-terminal-mod/hooks/register.js +66 -0
- package/config/model-router/claude-terminal-mod/hooks/routing.js +55 -0
- package/config/model-router/claude-terminal-mod/hooks/runtime.js +2 -0
- package/config/model-router/claude-terminal-mod/tests/native.test.ts +86 -0
- package/config/model-router/policy.default.mjs +97 -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 +6 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/mcp/managed-cli-interface.mjs +9 -4
- package/plugin/scripts/capability-claim-evidence.mjs +9 -2
- package/plugin/scripts/codex-hook-adapter.mjs +18 -9
- package/plugin/scripts/project-capture-queue.mjs +7 -1
- package/plugin/scripts/project-progression-hook.mjs +16 -7
- package/plugin/scripts/project-progression-outbox.mjs +96 -15
- package/plugin/scripts/project-progression-producer.mjs +12 -1
- package/plugin/scripts/project-progression-store.mjs +53 -1
- package/plugin/scripts/project-transition-hook.mjs +19 -8
- package/plugin/scripts/session-snapshot-hook.mjs +2 -1
- package/scripts/claude-terminal-mod.mjs +89 -0
- 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 +453 -0
- package/scripts/model-routing-launchers.mjs +209 -0
- package/scripts/model-routing-policy-promotion.mjs +203 -0
- package/scripts/model-terminal-gateway.mjs +310 -0
- package/scripts/model-terminal-launchers.mjs +300 -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 +50 -1
- package/scripts/release-qualification.mjs +9 -6
- package/scripts/security-guidance-codex-compat.mjs +142 -0
- package/scripts/user-model-prompt-hook.mjs +69 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Permanent POSIX native launchers (macOS/Linux). Windows shell launchers are unsupported.
|
|
3
|
+
// Explicit installation only; no login/Keychain changes.
|
|
4
|
+
// VS Code Codex APPLICATION settings belong on the UI client; Claude MACHINE settings
|
|
5
|
+
// belong on the extension host. A saved setting is not proof an existing process reloaded.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
|
|
14
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
15
|
+
const sha = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
16
|
+
const shellQuote = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
17
|
+
const KEYS = { codex: 'chatgpt.cliExecutable', 'claude-code': 'claudeCode.claudeProcessWrapper' };
|
|
18
|
+
|
|
19
|
+
function regular(file) {
|
|
20
|
+
const stat = fs.lstatSync(file);
|
|
21
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Regular file required: ${file}`);
|
|
22
|
+
return fs.realpathSync(file);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function discoverNativeCodex(extensionsRoot) {
|
|
26
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(extensionsRoot, 'extensions.json'), 'utf8'));
|
|
27
|
+
const entries = manifest.filter((entry) => entry.identifier?.id === 'openai.chatgpt');
|
|
28
|
+
if (entries.length !== 1) throw new Error('Exactly one registered Codex extension required');
|
|
29
|
+
const extension = fs.realpathSync(entries[0].location.path);
|
|
30
|
+
const root = fs.realpathSync(extensionsRoot);
|
|
31
|
+
if (!extension.startsWith(`${root}${path.sep}`)) throw new Error('Registered extension escaped extension root');
|
|
32
|
+
const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'windows' : process.platform;
|
|
33
|
+
const arch = process.arch === 'arm64' ? 'aarch64' : process.arch === 'x64' ? 'x86_64' : process.arch;
|
|
34
|
+
const binary = regular(path.join(extension, 'bin', `${platform}-${arch}`, process.platform === 'win32' ? 'codex.exe' : 'codex'));
|
|
35
|
+
fs.accessSync(binary, fs.constants.X_OK);
|
|
36
|
+
return binary;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function launcherInvocation({ harness, args, config, launcherPath }) {
|
|
40
|
+
let nativeArgs = [...args]; let binary;
|
|
41
|
+
if (harness === 'codex') binary = discoverNativeCodex(config.extensionsRoot);
|
|
42
|
+
else if (harness === 'claude-code') {
|
|
43
|
+
if (!path.isAbsolute(nativeArgs[0] || '')) throw new Error('Claude wrapper requires the native binary as its first argument');
|
|
44
|
+
binary = regular(nativeArgs.shift());
|
|
45
|
+
const root = fs.realpathSync(config.extensionsRoot);
|
|
46
|
+
if (!binary.startsWith(`${root}${path.sep}`) || path.basename(binary) !== (process.platform === 'win32' ? 'claude.exe' : 'claude')) throw new Error('Claude wrapper binary is not an extension native executable');
|
|
47
|
+
} else throw new Error('Unsupported native host');
|
|
48
|
+
if (binary === fs.realpathSync(launcherPath) || binary === fs.realpathSync(SELF)) throw new Error('Native launcher recursion refused');
|
|
49
|
+
// These extension probes/transport bridges do not submit model turns.
|
|
50
|
+
const probe = nativeArgs.length === 1 && ['--version', '--help'].includes(nativeArgs[0]);
|
|
51
|
+
const bridge = harness === 'codex' && nativeArgs.length === 2 && nativeArgs[0] === 'stdio-to-uds';
|
|
52
|
+
if (probe || bridge) return { command: binary, args: nativeArgs, routed: false };
|
|
53
|
+
regular(config.gatewayPath);
|
|
54
|
+
return { command: config.nodeBinary, args: [config.gatewayPath, '--harness', harness, '--real-binary', binary, '--', ...nativeArgs], routed: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function atomic(file, bytes, mode = 0o600) {
|
|
58
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
59
|
+
if (fs.existsSync(file)) regular(file);
|
|
60
|
+
const temp = `${file}.${crypto.randomUUID()}.tmp`;
|
|
61
|
+
const fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, mode);
|
|
62
|
+
try { fs.writeFileSync(fd, bytes); fs.fchmodSync(fd, mode); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
63
|
+
try { fs.renameSync(temp, file); } finally { if (fs.existsSync(temp)) fs.unlinkSync(temp); }
|
|
64
|
+
// Windows cannot fsync a directory handle through Node. The file was flushed
|
|
65
|
+
// above and replacement remains atomic; POSIX additionally flushes the directory.
|
|
66
|
+
if (process.platform === 'win32') return;
|
|
67
|
+
const dirFd = fs.openSync(path.dirname(file), fs.constants.O_RDONLY);
|
|
68
|
+
try { fs.fsyncSync(dirFd); } finally { fs.closeSync(dirFd); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function backup(file) {
|
|
72
|
+
if (!fs.existsSync(file)) return null;
|
|
73
|
+
regular(file);
|
|
74
|
+
const bytes = fs.readFileSync(file); const target = `${file}.${sha(bytes)}.original`;
|
|
75
|
+
if (fs.existsSync(target)) { if (sha(fs.readFileSync(regular(target))) !== sha(bytes)) throw new Error('Backup digest mismatch'); }
|
|
76
|
+
else { const fd = fs.openSync(target, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o400);
|
|
77
|
+
try { fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); } finally { fs.closeSync(fd); } }
|
|
78
|
+
return target;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Collect native gateway, policy engine and their literal local dependencies into an immutable snapshot. */
|
|
82
|
+
export function runtimeSnapshot(sourceRoot) {
|
|
83
|
+
const root = fs.realpathSync(sourceRoot); const files = new Map();
|
|
84
|
+
const visit = (relative) => {
|
|
85
|
+
if (files.has(relative)) return;
|
|
86
|
+
const file = regular(path.join(root, relative));
|
|
87
|
+
if (!file.startsWith(`${root}${path.sep}`)) throw new Error('Runtime dependency escaped source root');
|
|
88
|
+
const bytes = fs.readFileSync(file); files.set(relative, bytes);
|
|
89
|
+
for (const match of bytes.toString().matchAll(/(?:from\s*|import\(\s*)['"](\.[^'"]+)['"]/g)) {
|
|
90
|
+
visit(path.relative(root, path.resolve(path.dirname(file), match[1])));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
for (const relative of ['scripts/model-routing-gateway.mjs', 'scripts/model-router-engine.mjs', 'scripts/metaharness-router.mjs']) visit(relative);
|
|
94
|
+
for (const relative of ['scripts/model-terminal-gateway.mjs', 'scripts/claude-terminal-mod.mjs', 'scripts/model-terminal-launchers.mjs']) {
|
|
95
|
+
if (fs.existsSync(path.join(root, relative))) visit(relative);
|
|
96
|
+
}
|
|
97
|
+
const mod = 'config/model-router/claude-terminal-mod';
|
|
98
|
+
if (fs.existsSync(path.join(root, mod))) {
|
|
99
|
+
for (const relative of ['.claude-plugin/plugin.json', 'hooks/hooks.json', 'hooks/register.js', 'hooks/routing.js', 'hooks/runtime.js']) visit(`${mod}/${relative}`);
|
|
100
|
+
}
|
|
101
|
+
// The socket adapter must resolve its transport from the same immutable closure.
|
|
102
|
+
// Optional native accelerators are deliberately excluded; ws supports pure JS.
|
|
103
|
+
if (files.has('scripts/model-terminal-gateway.mjs')) {
|
|
104
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
105
|
+
const packageRoot = path.dirname(regular(require.resolve('ws/package.json')));
|
|
106
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json')));
|
|
107
|
+
if (packageJson.name !== 'ws') throw new Error('Unexpected WebSocket dependency identity');
|
|
108
|
+
for (const relative of ['package.json', 'LICENSE', 'index.js', 'wrapper.mjs', 'browser.js',
|
|
109
|
+
...fs.readdirSync(path.join(packageRoot, 'lib')).filter((name) => name.endsWith('.js')).map((name) => `lib/${name}`)]) {
|
|
110
|
+
const file = regular(path.join(packageRoot, relative));
|
|
111
|
+
if (!file.startsWith(`${packageRoot}${path.sep}`)) throw new Error('WebSocket dependency escaped package root');
|
|
112
|
+
files.set(`node_modules/ws/${relative}`, fs.readFileSync(file));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const name of fs.readdirSync(path.join(root, 'config/model-router'))) {
|
|
116
|
+
if (/\.(?:json|mjs)$/.test(name)) visit(`config/model-router/${name}`);
|
|
117
|
+
}
|
|
118
|
+
const identity = [...files].sort(([a], [b]) => a.localeCompare(b)).map(([file, bytes]) => [file, sha(bytes)]);
|
|
119
|
+
return { files, identity, digest: sha(JSON.stringify(identity)) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function stableNode() {
|
|
123
|
+
for (const file of ['/opt/homebrew/bin/node', process.execPath]) {
|
|
124
|
+
try { if (fs.realpathSync(file) === fs.realpathSync(process.execPath)) return file; } catch {}
|
|
125
|
+
}
|
|
126
|
+
return process.execPath;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Terminal routing does not require a VS Code extension installation. */
|
|
130
|
+
export function installRoutingRuntime({ sourceRoot, home = os.homedir(), apply = false } = {}) {
|
|
131
|
+
const snapshot = runtimeSnapshot(sourceRoot);
|
|
132
|
+
const runtimeRoot = path.join(home, '.cache/ruvnet-brain/model-routing/versions', snapshot.digest);
|
|
133
|
+
if (apply) for (const [relative, bytes] of snapshot.files) {
|
|
134
|
+
const file = path.join(runtimeRoot, relative);
|
|
135
|
+
if (fs.existsSync(file)) {
|
|
136
|
+
if (sha(fs.readFileSync(regular(file))) !== sha(bytes)) throw new Error('Immutable runtime snapshot mismatch');
|
|
137
|
+
} else atomic(file, bytes);
|
|
138
|
+
}
|
|
139
|
+
return { apply, runtimeRoot, runtimeDigest: snapshot.digest, runtimeFiles: snapshot.identity };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function installNativeLaunchers({ sourceRoot, home = os.homedir(), extensionsRoot = path.join(home, '.vscode-server/extensions'), nodeBinary = stableNode(), apply = false } = {}) {
|
|
143
|
+
// VS Code spawns its executable directly. A .cmd/.sh surrogate is not a proved
|
|
144
|
+
// Windows executable adapter; refuse before writing a misleading registration.
|
|
145
|
+
if (!['darwin', 'linux'].includes(process.platform)) throw new Error('Permanent native routing launchers support macOS/Linux only; Windows executable adapter is not implemented');
|
|
146
|
+
const snapshot = runtimeSnapshot(sourceRoot);
|
|
147
|
+
const base = path.join(home, '.cache/ruvnet-brain/model-routing');
|
|
148
|
+
const runtime = path.join(base, 'versions', snapshot.digest);
|
|
149
|
+
const runner = path.join(base, 'model-routing-launchers.mjs');
|
|
150
|
+
const configPath = path.join(base, 'launcher-config.json');
|
|
151
|
+
const config = { extensionsRoot: fs.realpathSync(extensionsRoot), nodeBinary: (regular(fs.realpathSync(nodeBinary)), nodeBinary), gatewayPath: path.join(runtime, 'scripts/model-routing-gateway.mjs'), terminalGatewayPath: path.join(runtime, 'scripts/model-terminal-gateway.mjs'), runtimeRoot: runtime, runtimeDigest: snapshot.digest };
|
|
152
|
+
const codexBinary = discoverNativeCodex(config.extensionsRoot);
|
|
153
|
+
const launchers = Object.fromEntries(Object.keys(KEYS).map((host) => [host, path.join(home, '.local/bin', `ruvnet-brain-${host === 'codex' ? 'codex' : 'claude'}-gateway`)]));
|
|
154
|
+
const texts = Object.fromEntries(Object.entries(launchers).map(([host]) => [host, `#!/bin/sh\n# Managed RuvNet Brain native routing launcher\nexec ${shellQuote(config.nodeBinary)} ${shellQuote(runner)} --launch ${shellQuote(host)} --config ${shellQuote(configPath)} --launcher "$0" -- "$@"\n`]));
|
|
155
|
+
for (const [host, file] of Object.entries(launchers)) {
|
|
156
|
+
if (fs.existsSync(file) && !fs.readFileSync(regular(file), 'utf8').includes('# Managed RuvNet Brain native routing launcher')) throw new Error(`Preserving unmanaged launcher: ${file}`);
|
|
157
|
+
}
|
|
158
|
+
const receipt = { apply, launchers, config, codexBinary, settings: Object.fromEntries(Object.entries(KEYS).map(([host, key]) => [key, launchers[host]])), runtimeFiles: snapshot.identity };
|
|
159
|
+
if (!apply) return receipt;
|
|
160
|
+
for (const [relative, bytes] of snapshot.files) {
|
|
161
|
+
const file = path.join(runtime, relative);
|
|
162
|
+
if (fs.existsSync(file)) { if (sha(fs.readFileSync(regular(file))) !== sha(bytes)) throw new Error('Immutable runtime snapshot mismatch'); }
|
|
163
|
+
else atomic(file, bytes);
|
|
164
|
+
}
|
|
165
|
+
for (const file of [runner, configPath, ...Object.values(launchers)]) backup(file);
|
|
166
|
+
atomic(runner, fs.readFileSync(SELF)); atomic(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
167
|
+
for (const [host, file] of Object.entries(launchers)) atomic(file, texts[host], 0o755);
|
|
168
|
+
return receipt;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Apply only absent or identical settings. Conflicting user overrides are preserved and disclosed. */
|
|
172
|
+
export function updateLauncherSettings(file, requested, { apply = false } = {}) {
|
|
173
|
+
const text = fs.existsSync(file) ? fs.readFileSync(regular(file), 'utf8') : '{}\n';
|
|
174
|
+
let settings;
|
|
175
|
+
try { settings = JSON.parse(text); } catch { throw new Error('Settings are JSONC or malformed; refusing a destructive rewrite'); }
|
|
176
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) throw new Error('Settings object required');
|
|
177
|
+
const add = {}; const preserved = {};
|
|
178
|
+
for (const [key, value] of Object.entries(requested)) {
|
|
179
|
+
if (!Object.values(KEYS).includes(key)) throw new Error('Unsupported launcher setting');
|
|
180
|
+
if (settings[key] === undefined || settings[key] === null || settings[key] === '') add[key] = value;
|
|
181
|
+
else if (settings[key] !== value) preserved[key] = settings[key];
|
|
182
|
+
}
|
|
183
|
+
Object.assign(settings, add);
|
|
184
|
+
const changed = Object.keys(add).length > 0;
|
|
185
|
+
const receipt = { file, apply, changed, preservedOverrides: preserved, settings: requested, backup: null };
|
|
186
|
+
if (apply && changed) { receipt.backup = backup(file); atomic(file, `${JSON.stringify(settings, null, 2)}\n`, fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : 0o600); }
|
|
187
|
+
return receipt;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === SELF) {
|
|
191
|
+
const argv = process.argv.slice(2);
|
|
192
|
+
if (argv[0] === '--launch') {
|
|
193
|
+
try {
|
|
194
|
+
const harness = argv[1]; const configPath = argv[3]; const launcherPath = argv[5];
|
|
195
|
+
if (argv[2] !== '--config' || argv[4] !== '--launcher' || argv[6] !== '--') throw new Error('Invalid launcher arguments');
|
|
196
|
+
const invocation = launcherInvocation({ harness, args: argv.slice(7), config: JSON.parse(fs.readFileSync(regular(configPath))), launcherPath });
|
|
197
|
+
const child = spawn(invocation.command, invocation.args, { stdio: 'inherit', shell: false, env: process.env });
|
|
198
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => child.kill(signal));
|
|
199
|
+
child.once('error', () => { process.stderr.write('Native routing launcher unavailable\n'); process.exitCode = 1; });
|
|
200
|
+
child.once('exit', (code) => { process.exitCode = code ?? 1; });
|
|
201
|
+
} catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }
|
|
202
|
+
} else {
|
|
203
|
+
try {
|
|
204
|
+
const apply = argv.includes('--apply'); const sourceAt = argv.indexOf('--source-root');
|
|
205
|
+
if (sourceAt < 0 || !argv[sourceAt + 1] || argv.some((arg, i) => !['--apply', '--source-root'].includes(arg) && i !== sourceAt + 1)) throw new Error('Usage: model-routing-launchers.mjs --source-root ABSOLUTE_PATH [--apply]');
|
|
206
|
+
console.log(JSON.stringify(installNativeLaunchers({ sourceRoot: argv[sourceAt + 1], apply }), null, 2));
|
|
207
|
+
} catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Promotion authority is a separate reviewed contract, never candidate self-evaluation.
|
|
2
|
+
// This module does not discover models, execute inference, alter profiles or control billing.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
8
|
+
const object = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
9
|
+
const canonical = (v) => JSON.stringify(Array.isArray(v) ? v.map((x) => JSON.parse(canonical(x)))
|
|
10
|
+
: object(v) ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, JSON.parse(canonical(v[k]))])) : v);
|
|
11
|
+
export const candidateSha256 = (policy) => sha256(canonical(policy));
|
|
12
|
+
const same = (a, b) => canonical(a) === canonical(b);
|
|
13
|
+
const sha = (v) => typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
|
|
14
|
+
const hosts = { codex: 'openai', 'claude-code': 'anthropic' };
|
|
15
|
+
const kinds = ['availability', 'settings', 'handoff', 'role-quality'];
|
|
16
|
+
const time = (v) => typeof v === 'string' && Number.isFinite(Date.parse(v));
|
|
17
|
+
const fail = (reason) => ({ ok: false, status: 'blocked', reason });
|
|
18
|
+
|
|
19
|
+
function roleRoutes(policy) {
|
|
20
|
+
const rows = [];
|
|
21
|
+
for (const [host, routes] of Object.entries(policy.routes)) {
|
|
22
|
+
if (!hosts[host] || !object(routes)) throw new Error('Unknown native harness');
|
|
23
|
+
for (const [role, route] of Object.entries(routes)) {
|
|
24
|
+
if (role === 'codingEffort') {
|
|
25
|
+
if (typeof route !== 'string' || !routes.medium?.model) throw new Error('Invalid coding effort rule');
|
|
26
|
+
rows.push({ host, role, model: routes.medium.model, effort: route });
|
|
27
|
+
} else {
|
|
28
|
+
if (!object(route) || typeof route.model !== 'string' || typeof route.effort !== 'string'
|
|
29
|
+
|| Object.keys(route).some((k) => !['model', 'effort', 'requiresNamedReason'].includes(k))
|
|
30
|
+
|| (route.requiresNamedReason !== undefined && typeof route.requiresNamedReason !== 'boolean')) throw new Error('Invalid role route');
|
|
31
|
+
rows.push({ host, role, model: route.model, effort: route.effort });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return rows;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** contract must come from the trusted reviewer/owner, independently of the proposal.
|
|
39
|
+
* trustedReceipts binds exact immutable receipt bytes, not mutable source URLs or labels.
|
|
40
|
+
* qualityFloors is role-specific: numeric metrics retain their native scales and direction.
|
|
41
|
+
* Passing untrusted proposal data as contract defeats this trust boundary; the runner must not do so.
|
|
42
|
+
*/
|
|
43
|
+
export function validateRoutingProposal({ currentPolicy, candidatePolicy, evidence = [], contract, sourceSha,
|
|
44
|
+
now = Date.now(), overrides = {} } = {}) {
|
|
45
|
+
try {
|
|
46
|
+
if (!object(currentPolicy) || !object(candidatePolicy) || currentPolicy.schemaVersion !== 1
|
|
47
|
+
|| candidatePolicy.schemaVersion !== 1 || !time(currentPolicy.reviewedAt) || !object(currentPolicy.routes) || !object(candidatePolicy.routes)) return fail('Invalid policy schema');
|
|
48
|
+
const omit = (p) => Object.fromEntries(Object.entries(p).filter(([k]) => !['routes', 'reviewedAt', 'policyRevisionAt'].includes(k)));
|
|
49
|
+
if (!same(omit(currentPolicy), omit(candidatePolicy))) return fail('Non-route settings or billing/control mutation');
|
|
50
|
+
if (!time(candidatePolicy.reviewedAt) || Date.parse(candidatePolicy.reviewedAt) > now
|
|
51
|
+
|| Date.parse(candidatePolicy.reviewedAt) < Date.parse(currentPolicy.reviewedAt)) return fail('Invalid review date');
|
|
52
|
+
if (candidatePolicy.policyRevisionAt !== currentPolicy.policyRevisionAt
|
|
53
|
+
&& (!time(candidatePolicy.policyRevisionAt) || Date.parse(candidatePolicy.policyRevisionAt) > now)) return fail('Invalid revision date');
|
|
54
|
+
if (contract?.schemaVersion !== undefined && ![1, 2].includes(contract.schemaVersion)) return fail('Unsupported qualification contract version');
|
|
55
|
+
const configuredTurnEvidence = contract?.schemaVersion === 2;
|
|
56
|
+
if (configuredTurnEvidence && (contract.identityEvidence !== 'native-configured-turn' || contract.backendIdentityProved !== false
|
|
57
|
+
|| !object(contract.reviewer) || typeof contract.reviewer.model !== 'string' || typeof contract.reviewer.effort !== 'string'
|
|
58
|
+
|| !hosts[contract.reviewer.host])) return fail('Explicit native configured-turn qualification boundary required');
|
|
59
|
+
const prior = roleRoutes(currentPolicy); const next = roleRoutes(candidatePolicy);
|
|
60
|
+
if (!same(prior.map((r) => [r.host, r.role]).sort(), next.map((r) => [r.host, r.role]).sort())) return fail('Native provider or role expansion');
|
|
61
|
+
const changed = next.filter((r) => !same(r, prior.find((p) => p.host === r.host && p.role === r.role)));
|
|
62
|
+
if (!changed.length && candidatePolicy.reviewedAt === currentPolicy.reviewedAt
|
|
63
|
+
&& candidatePolicy.policyRevisionAt === currentPolicy.policyRevisionAt) return { ok: true, status: 'unchanged', candidateSha: candidateSha256(candidatePolicy), qualifiedRoles: [] };
|
|
64
|
+
if (!sha(sourceSha) || !object(contract) || contract.sourceSha !== sourceSha || contract.authority !== 'independent-reviewed'
|
|
65
|
+
|| !sha(contract.contractSha256) || contract.contractSha256 !== candidateSha256(Object.fromEntries(Object.entries(contract).filter(([k]) => k !== 'contractSha256')))) return fail('Missing or unmatched reviewed authority contract');
|
|
66
|
+
if (!object(contract.allowedRoutes) || !object(contract.qualityFloors) || !object(contract.trustedReceipts)
|
|
67
|
+
|| !Array.isArray(contract.trustedSourceIds) || !contract.trustedSourceIds.length || contract.trustedSourceIds.some((id) => !sha(id))
|
|
68
|
+
|| !Number.isFinite(contract.maxEvidenceAgeMs) || contract.maxEvidenceAgeMs <= 0) return fail('Incomplete qualification contract');
|
|
69
|
+
const digest = candidateSha256(candidatePolicy);
|
|
70
|
+
// Research freshness has its own receipt. Unchanged approved routes never become a
|
|
71
|
+
// promotion, nor acquire a new policy review date from inventory or semantic research.
|
|
72
|
+
if (!changed.length) return fail('Unchanged routes preserve policy dates; record research review separately');
|
|
73
|
+
const required = changed;
|
|
74
|
+
for (const route of next) {
|
|
75
|
+
const allowed = contract.allowedRoutes[`${route.host}/${route.role}`];
|
|
76
|
+
if (changed.some((r) => r.host === route.host && r.role === route.role) && (!Array.isArray(allowed) || !allowed.some((a) => a.model === route.model && a.effort === route.effort
|
|
77
|
+
&& a.provider === hosts[route.host] && a.nativeSubscription === true))) return fail(`Unsupported native allocation: ${route.host}/${route.role}`);
|
|
78
|
+
if (overrides[route.host]?.[route.role] !== undefined
|
|
79
|
+
&& !same(route, prior.find((p) => p.host === route.host && p.role === route.role))) return fail('Explicit user override is preserved');
|
|
80
|
+
const old = currentPolicy.routes[route.host][route.role]; const value = candidatePolicy.routes[route.host][route.role];
|
|
81
|
+
if (object(old) && old.requiresNamedReason !== value.requiresNamedReason) return fail('Named-reason control is preserved');
|
|
82
|
+
}
|
|
83
|
+
for (const route of required) {
|
|
84
|
+
const binding = (row) => row.sourceSha === sourceSha && row.candidateSha === digest && row.host === route.host
|
|
85
|
+
&& row.role === route.role && row.model === route.model && row.effort === route.effort
|
|
86
|
+
&& row.nativeObservedIdentity === route.model && row.nativeObservedEffort === route.effort
|
|
87
|
+
&& row.harness === route.host && typeof row.harnessVersion === 'string' && !!row.harnessVersion;
|
|
88
|
+
for (const kind of kinds) {
|
|
89
|
+
const row = evidence.find((r) => object(r) && r.kind === kind && binding(r));
|
|
90
|
+
if (!row || !Array.isArray(row.sourceIds) || !row.sourceIds.length
|
|
91
|
+
|| row.sourceIds.some((id) => !sha(id) || !contract.trustedSourceIds.includes(id)) || !time(row.checkedAt) || Date.parse(row.checkedAt) > now || now - Date.parse(row.checkedAt) > contract.maxEvidenceAgeMs
|
|
92
|
+
|| !sha(row.receiptSha256) || contract.trustedReceipts[row.receiptSha256] !== candidateSha256(Object.fromEntries(Object.entries(row).filter(([k]) => k !== 'receiptSha256')))) return fail(`Missing trusted ${kind} evidence: ${route.host}/${route.role}`);
|
|
93
|
+
if (configuredTurnEvidence && (row.identityEvidence !== 'native-configured-turn' || row.backendIdentityProved !== false
|
|
94
|
+
|| typeof row.nativeSessionId !== 'string' || !row.nativeSessionId || typeof row.nativeTurnId !== 'string' || !row.nativeTurnId
|
|
95
|
+
|| !sha(row.transcriptSha256))) return fail('Completed native configured-turn transcript binding missing');
|
|
96
|
+
if (kind === 'availability' && (row.available !== true || row.nativeSubscription !== true || row.provider !== hosts[route.host])) return fail('Native subscription availability unverified');
|
|
97
|
+
if (kind === 'settings' && row.supported !== true) return fail('Native settings unsupported');
|
|
98
|
+
if (configuredTurnEvidence && kind === 'handoff' && row.identityReturnedBasis !== 'native-host-confirmed-configuration') return fail('Native handoff configured identity basis missing');
|
|
99
|
+
if (kind === 'handoff' && (row.completed !== true || row.identityReturned !== true || row.effortObserved !== true)) return fail('Native handoff identity/effort unverified');
|
|
100
|
+
if (kind === 'role-quality') {
|
|
101
|
+
if (configuredTurnEvidence && (row.reviewerHost !== contract.reviewer.host || row.reviewerModel !== contract.reviewer.model || row.reviewerEffort !== contract.reviewer.effort
|
|
102
|
+
|| row.reviewerModel === route.model)) return fail('Separately frozen independent model reviewer required');
|
|
103
|
+
const floors = contract.qualityFloors[`${route.host}/${route.role}`];
|
|
104
|
+
if (row.reviewedBy !== 'independent-reviewer' || row.reviewedOutcome !== 'accepted' || row.selfEvaluation === true
|
|
105
|
+
|| !object(row.benchmark) || !row.benchmark.suite || !row.benchmark.version || !object(row.metrics)
|
|
106
|
+
|| !object(floors) || !Object.keys(floors).length) return fail('Independent role-quality qualification missing');
|
|
107
|
+
for (const [metric, floor] of Object.entries(floors)) {
|
|
108
|
+
if (!object(floor) || !Number.isFinite(floor.value) || !['minimum', 'maximum'].includes(floor.direction)
|
|
109
|
+
|| !Number.isFinite(row.metrics[metric]) || row.benchmark.suite !== floor.suite || row.benchmark.version !== floor.version
|
|
110
|
+
|| (floor.direction === 'minimum' ? row.metrics[metric] < floor.value : row.metrics[metric] > floor.value)) return fail(`Role-quality floor unmet: ${metric}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { ok: true, status: 'qualified', candidateSha: digest, qualifiedRoles: required.map((r) => `${r.host}/${r.role}`) };
|
|
116
|
+
} catch (error) { return fail(error.message); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function syncDir(dir) {
|
|
120
|
+
// Windows cannot flush directory descriptors through Node. File contents still fsync
|
|
121
|
+
// before rename; do not claim POSIX directory-entry crash durability on Windows.
|
|
122
|
+
if (process.platform === 'win32') return;
|
|
123
|
+
const fd = fs.openSync(dir, 'r'); try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
124
|
+
}
|
|
125
|
+
function writeExclusive(file, bytes) {
|
|
126
|
+
const fd = fs.openSync(file, 'wx', 0o600);
|
|
127
|
+
try { fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
128
|
+
syncDir(path.dirname(file));
|
|
129
|
+
}
|
|
130
|
+
function atomicReplace(file, bytes) {
|
|
131
|
+
const temporary = `${file}.${crypto.randomUUID()}.tmp`;
|
|
132
|
+
try { writeExclusive(temporary, bytes); fs.renameSync(temporary, file); syncDir(path.dirname(file)); }
|
|
133
|
+
finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
|
|
134
|
+
}
|
|
135
|
+
function locked(policyPath, action) {
|
|
136
|
+
const lock = `${policyPath}.promotion.lock`; let acquired = false;
|
|
137
|
+
try { writeExclusive(lock, JSON.stringify({ pid: process.pid })); acquired = true; return action(); }
|
|
138
|
+
catch (error) { return fail(error.code === 'EEXIST' && !acquired ? 'Concurrent promotion owns lock' : error.message); }
|
|
139
|
+
finally { if (acquired) { fs.unlinkSync(lock); syncDir(path.dirname(lock)); } }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** CAS under an exclusive lock; only policyPath changes. Existing profile/policy.mjs overrides are untouched. */
|
|
143
|
+
export function promoteRoutingPolicy({ policyPath, candidatePolicy, expectedPriorSha, evidence, contract, sourceSha,
|
|
144
|
+
now = Date.now(), overrides, beforeCommit } = {}) {
|
|
145
|
+
if (!sha(expectedPriorSha) || typeof policyPath !== 'string' || !path.isAbsolute(policyPath)) return fail('Absolute policy path and expected prior SHA required');
|
|
146
|
+
return locked(policyPath, () => {
|
|
147
|
+
if (fs.lstatSync(policyPath).isSymbolicLink()) return fail('Policy symlink mutation refused');
|
|
148
|
+
const prior = fs.readFileSync(policyPath); const currentPolicy = JSON.parse(prior);
|
|
149
|
+
const bytes = Buffer.from(`${JSON.stringify(candidatePolicy, null, 2)}\n`); const nextSha = sha256(bytes);
|
|
150
|
+
const archiveDir = `${policyPath}.history`;
|
|
151
|
+
if (sha256(prior) !== expectedPriorSha) {
|
|
152
|
+
const receiptPath = path.join(archiveDir, `${nextSha}.receipt.json`);
|
|
153
|
+
if (sha256(prior) === nextSha && fs.existsSync(receiptPath)) {
|
|
154
|
+
const receipt = JSON.parse(fs.readFileSync(receiptPath));
|
|
155
|
+
if (receipt.sourceSha === sourceSha && receipt.priorSha === expectedPriorSha) return { ok: true, status: 'idempotent', ...receipt };
|
|
156
|
+
}
|
|
157
|
+
return fail('Prior policy changed; fencing rejected');
|
|
158
|
+
}
|
|
159
|
+
const validation = validateRoutingProposal({ currentPolicy, candidatePolicy, evidence, contract, sourceSha, now, overrides });
|
|
160
|
+
if (!validation.ok || validation.status === 'unchanged') return validation;
|
|
161
|
+
fs.mkdirSync(archiveDir, { recursive: true, mode: 0o700 });
|
|
162
|
+
if (fs.lstatSync(archiveDir).isSymbolicLink()) return fail('Archive symlink mutation refused');
|
|
163
|
+
const previousPath = path.join(archiveDir, `${expectedPriorSha}.policy.json`);
|
|
164
|
+
const candidatePath = path.join(archiveDir, `${new Date(now).toISOString().replaceAll(':', '-')}-${nextSha}.policy.json`);
|
|
165
|
+
for (const [file, body] of [[previousPath, prior], [candidatePath, bytes]]) {
|
|
166
|
+
if (fs.existsSync(file)) { if (sha256(fs.readFileSync(file)) !== sha256(body)) return fail('Archive digest mismatch'); }
|
|
167
|
+
else writeExclusive(file, body);
|
|
168
|
+
}
|
|
169
|
+
const receipt = { schemaVersion: 1, directorySync: process.platform === 'win32' ? 'unsupported' : 'performed', promotedAt: new Date(now).toISOString(), sourceSha,
|
|
170
|
+
candidateSha: validation.candidateSha, priorSha: expectedPriorSha, policySha: nextSha,
|
|
171
|
+
previousPath, candidatePath, contractSha256: contract.contractSha256, qualifiedRoles: validation.qualifiedRoles,
|
|
172
|
+
...(contract.schemaVersion === 2 ? { identityEvidence: 'native-configured-turn', backendIdentityProved: false,
|
|
173
|
+
qualificationScope: 'bounded local role non-regression; not general superiority or subscription savings' } : {}) };
|
|
174
|
+
const receiptPath = path.join(archiveDir, `${nextSha}.receipt.json`);
|
|
175
|
+
if (!fs.existsSync(receiptPath)) writeExclusive(receiptPath, JSON.stringify(receipt, null, 2));
|
|
176
|
+
if (beforeCommit) beforeCommit(); // fault injection / integration fence; no arbitrary proposal callback.
|
|
177
|
+
if (sha256(fs.readFileSync(policyPath)) !== expectedPriorSha) return fail('Prior policy changed before atomic commit');
|
|
178
|
+
try { atomicReplace(policyPath, bytes); } catch (error) {
|
|
179
|
+
// A rename may have succeeded before directory fsync failed: restore under the same fence.
|
|
180
|
+
if (sha256(fs.readFileSync(policyPath)) === nextSha) {
|
|
181
|
+
try { atomicReplace(policyPath, prior); }
|
|
182
|
+
catch { return { ok: false, status: 'degraded', reason: 'Commit durability failed and rollback failed', policyMayHaveChanged: true }; }
|
|
183
|
+
}
|
|
184
|
+
return fail(`Atomic commit failed; prior policy retained: ${error.message}`);
|
|
185
|
+
}
|
|
186
|
+
return { ok: true, status: 'promoted', ...receipt };
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function rollbackRoutingPolicy({ policyPath, expectedCurrentSha, previousSha } = {}) {
|
|
191
|
+
if (typeof policyPath !== 'string' || !path.isAbsolute(policyPath) || !sha(expectedCurrentSha) || !sha(previousSha)) return fail('Rollback requires exact policy SHA fence');
|
|
192
|
+
return locked(policyPath, () => {
|
|
193
|
+
if (fs.lstatSync(policyPath).isSymbolicLink()) return fail('Policy symlink mutation refused');
|
|
194
|
+
const current = fs.readFileSync(policyPath);
|
|
195
|
+
if (sha256(current) !== expectedCurrentSha) return fail('Rollback fencing rejected');
|
|
196
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(`${policyPath}.history`, `${expectedCurrentSha}.receipt.json`)));
|
|
197
|
+
if (receipt.priorSha !== previousSha) return fail('Rollback is not this promotion predecessor');
|
|
198
|
+
const prior = fs.readFileSync(path.join(`${policyPath}.history`, `${previousSha}.policy.json`));
|
|
199
|
+
if (sha256(prior) !== previousSha) return fail('Rollback archive digest mismatch');
|
|
200
|
+
atomicReplace(policyPath, prior);
|
|
201
|
+
return { ok: true, status: 'rolled-back', policySha: previousSha };
|
|
202
|
+
});
|
|
203
|
+
}
|