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.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +144 -27
  3. package/config/model-router/catalog.template.json +126 -52
  4. package/config/model-router/policy.default.mjs +94 -75
  5. package/config/model-router/qualification-contract.json +124 -0
  6. package/config/model-router/routing-eval-cases.json +275 -0
  7. package/config/model-router/routing-policy.template.json +76 -0
  8. package/config/model-router/weekly-analyst-instruction.md +60 -0
  9. package/data/model-catalog.json +44 -49
  10. package/package.json +3 -1
  11. package/plugin/.claude-plugin/plugin.json +1 -1
  12. package/plugin/.codex-plugin/plugin.json +1 -1
  13. package/plugin/scripts/codex-hook-adapter.mjs +18 -9
  14. package/scripts/codex-hook-trust-reconcile.mjs +247 -0
  15. package/scripts/codex-routed.sh +3 -36
  16. package/scripts/goldie-weekly.sh +8 -64
  17. package/scripts/metaharness-router.mjs +7 -1
  18. package/scripts/model-analyst-sandbox.mjs +54 -0
  19. package/scripts/model-currency-evidence.mjs +139 -0
  20. package/scripts/model-currency.mjs +230 -0
  21. package/scripts/model-native-catalog.mjs +111 -0
  22. package/scripts/model-native-qualification.mjs +251 -0
  23. package/scripts/model-router-agent-hook.mjs +136 -0
  24. package/scripts/model-router-dispatch.mjs +161 -0
  25. package/scripts/model-router-engine.mjs +155 -104
  26. package/scripts/model-routing-eval.mjs +108 -0
  27. package/scripts/model-routing-gateway.mjs +420 -0
  28. package/scripts/model-routing-launchers.mjs +174 -0
  29. package/scripts/model-routing-policy-promotion.mjs +203 -0
  30. package/scripts/model-weekly-analyst.mjs +299 -0
  31. package/scripts/model-weekly-assessment.mjs +91 -0
  32. package/scripts/model-weekly-cycle.mjs +183 -0
  33. package/scripts/model-weekly-qualification.mjs +362 -0
  34. package/scripts/native-subscription-usage.mjs +57 -0
  35. package/scripts/release-qualification-contract.mjs +41 -0
  36. package/scripts/security-guidance-codex-compat.mjs +142 -0
  37. package/scripts/user-model-prompt-hook.mjs +69 -0
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ // DISTINCT-FROM: scripts/model-router-catalog.mjs — per-user evidence currency, never candidate/default mutation.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { spawn } from 'node:child_process';
7
+ import { randomUUID } from 'node:crypto';
8
+ import { buildWeeklyAssessment } from './model-weekly-assessment.mjs';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { WEEK_MS, digest, parseInventory, parseArtificialAnalysis, parseCodingAgentEvidence, currencyStatus } from './model-currency-evidence.mjs';
11
+
12
+ export const DEFAULT_ROUTER_DIR = path.join(os.homedir(), '.claude', 'model-router');
13
+ export const AA_URLS = [
14
+ 'https://artificialanalysis.ai/models/comparisons',
15
+ 'https://artificialanalysis.ai/models/comparisons/claude-fable-5-1-medium-vs-claude-opus-5-medium',
16
+ 'https://artificialanalysis.ai/models/releases/comparisons/gpt-6-1-sol-vs-claude-sonnet-5-5',
17
+ 'https://artificialanalysis.ai/models/releases/comparisons/gpt-6-luna-vs-gpt-6-astra',
18
+ 'https://artificialanalysis.ai/models/releases/comparisons/claude-opus-5-5-vs-gpt-6-astra',
19
+ ];
20
+ export const OFFICIAL_SOURCES = [
21
+ { provider: 'openai', url: 'https://developers.openai.com/api/docs/models' },
22
+ { provider: 'anthropic', url: 'https://platform.claude.com/docs/en/models/overview' },
23
+ ];
24
+ export const AGENT_SOURCE_URLS = [
25
+ 'https://artificialanalysis.ai/agents/coding-agents',
26
+ 'https://artificialanalysis.ai/methodology/coding-agents-benchmarking',
27
+ ];
28
+ export const CODING_BENCHMARK_SOURCES = [
29
+ { suite: 'vulcanbench', url: 'https://vulcanbench.com/' },
30
+ { suite: 'vulcanbench-swe-v4', url: 'https://vulcanbench.com/benchmarks/swe-v4-gpt61-sol-v318.html' },
31
+ { suite: 'terminal-bench', url: 'https://www.tbench.ai/' },
32
+ { suite: 'swe-bench', url: 'https://www.swebench.com/' },
33
+ ];
34
+ const INVENTORY_URL = 'https://openrouter.ai/api/v1/models';
35
+ const LOCK_MS = 10 * 60 * 1000;
36
+ const RETRY_MS = 60 * 60 * 1000;
37
+ const SELF = fileURLToPath(import.meta.url);
38
+ function readRecord(routerDir) {
39
+ try {
40
+ const target = path.join(routerDir, 'currency.json');
41
+ if (fs.statSync(target).size > 8 * 1024 * 1024) return null;
42
+ return JSON.parse(fs.readFileSync(target, 'utf8'));
43
+ } catch { return null; }
44
+ }
45
+ export function readCurrencyStatus({ routerDir = DEFAULT_ROUTER_DIR, now = Date.now() } = {}) {
46
+ return currencyStatus(readRecord(routerDir), now);
47
+ }
48
+ function atomicWrite(target, bytes, beforeCommit = () => {}) {
49
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
50
+ const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
51
+ try {
52
+ const fd = fs.openSync(tmp, 'wx', 0o600);
53
+ try { fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
54
+ beforeCommit();
55
+ fs.renameSync(tmp, target);
56
+ } finally { try { fs.unlinkSync(tmp); } catch { /* renamed */ } }
57
+ }
58
+ // This guard is held only for synchronous filesystem transactions, never during fetches.
59
+ // Never reap it: a crash inside this tiny transaction must fail closed rather than overlap writers.
60
+ function transaction(routerDir, operation) {
61
+ fs.mkdirSync(routerDir, { recursive: true, mode: 0o700 });
62
+ const guard = path.join(routerDir, 'currency-mutation.lock');
63
+ try { fs.mkdirSync(guard, { mode: 0o700 }); } catch (error) { if (error.code === 'EEXIST') return null; throw error; }
64
+ try { return operation(); } finally { fs.rmdirSync(guard); }
65
+ }
66
+ function ownerPath(routerDir) { return path.join(routerDir, 'currency-refresh-owner.json'); }
67
+ function readOwner(routerDir) {
68
+ try { return JSON.parse(fs.readFileSync(ownerPath(routerDir), 'utf8')); }
69
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
70
+ }
71
+ function claim(routerDir, now) {
72
+ return transaction(routerDir, () => {
73
+ const owner = readOwner(routerDir);
74
+ if (owner && (!Number.isFinite(owner.claimedAt) || now - owner.claimedAt <= LOCK_MS)) return null;
75
+ const next = { token: randomUUID(), claimedAt: now };
76
+ atomicWrite(ownerPath(routerDir), JSON.stringify(next));
77
+ return next.token;
78
+ });
79
+ }
80
+ function release(routerDir, token) {
81
+ return transaction(routerDir, () => {
82
+ if (readOwner(routerDir)?.token !== token) return false;
83
+ fs.unlinkSync(ownerPath(routerDir)); return true;
84
+ });
85
+ }
86
+ function fencedWrite(routerDir, token, target, bytes) {
87
+ const written = transaction(routerDir, () => {
88
+ if (readOwner(routerDir)?.token !== token) throw new Error('refresh superseded: ownership token changed');
89
+ atomicWrite(target, bytes, () => {
90
+ if (readOwner(routerDir)?.token !== token) throw new Error('refresh superseded before commit');
91
+ }); return true;
92
+ });
93
+ if (!written) throw new Error('refresh transaction busy; no write committed');
94
+ }
95
+
96
+ /** Prompt path: local bounded read and one detached worker; never await a network request. */
97
+ export function maybeLaunchCurrencyRefresh({ routerDir = DEFAULT_ROUTER_DIR, now = Date.now(), launch = spawn } = {}) {
98
+ const record = readRecord(routerDir); const status = currencyStatus(record, now);
99
+ if (status.status === 'current') return { ...status, launched: false };
100
+ const attempted = Date.parse(record?.lastAttempt?.checkedAt);
101
+ if (Number.isFinite(attempted) && attempted <= now && now - attempted < RETRY_MS) return { ...status, launched: false, deferred: 'retry cooldown' };
102
+ const lock = claim(routerDir, now);
103
+ if (!lock) return { ...status, launched: false, deferred: fs.existsSync(path.join(routerDir, 'currency-mutation.lock'))
104
+ ? 'refresh transaction blocked; inspect mutation guard before recovery' : 'refresh already running' };
105
+ try {
106
+ const child = launch(process.execPath, [SELF, '--refresh', '--claim-token', lock, '--router-dir', routerDir], { detached: true, stdio: 'ignore' });
107
+ child.once?.('error', () => release(routerDir, lock));
108
+ child.unref();
109
+ return { ...status, launched: true };
110
+ } catch (error) { release(routerDir, lock); return { ...status, launched: false, errors: [error.message] }; }
111
+ }
112
+
113
+ export async function refreshModelCurrency({ routerDir = DEFAULT_ROUTER_DIR, now = Date.now(), fetchImpl = fetch,
114
+ identityBindings, aaUrls = AA_URLS, officialSources = OFFICIAL_SOURCES, agentSourceUrls = AGENT_SOURCE_URLS, codingBenchmarkSources = CODING_BENCHMARK_SOURCES, claimToken = null } = {}) {
115
+ const lock = claimToken ?? claim(routerDir, now);
116
+ if (!lock) return { action: 'busy', status: 'stale', reason: 'refresh ownership or transaction guard unavailable' };
117
+ try {
118
+ const prior = readRecord(routerDir) ?? { schemaVersion: 1 };
119
+ const checkedAt = new Date(now).toISOString(); const errors = [];
120
+ let bindings = identityBindings;
121
+ if (!bindings) {
122
+ try { bindings = JSON.parse(fs.readFileSync(path.join(routerDir, 'identity-bindings.json'), 'utf8')); }
123
+ catch { bindings = {}; }
124
+ }
125
+ const collect = async (url) => {
126
+ const response = await fetchImpl(url, { signal: AbortSignal.timeout(20_000) });
127
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
128
+ const bytes = await response.text();
129
+ if (bytes.length > 6 * 1024 * 1024) throw new Error('source exceeds 6 MiB limit');
130
+ const source = { url, checkedAt, sha256: digest(bytes) };
131
+ fencedWrite(routerDir, lock, path.join(routerDir, 'evidence', `${source.sha256}.${url === INVENTORY_URL ? 'json' : 'html'}`), bytes);
132
+ return { source, bytes };
133
+ };
134
+ const results = await Promise.allSettled([INVENTORY_URL, ...aaUrls, ...officialSources.map((s) => s.url), ...agentSourceUrls, ...codingBenchmarkSources.map((s) => s.url)].map(collect));
135
+ let inventory = prior.inventory; let evaluations = prior.evaluations;
136
+ if (results[0].status === 'fulfilled') {
137
+ try { inventory = parseInventory(results[0].value.bytes, results[0].value.source); }
138
+ catch (error) { errors.push(`inventory: ${error.message}`); }
139
+ } else errors.push(`inventory: ${results[0].reason.message}`);
140
+ const parsed = [];
141
+ for (let i = 1; i <= aaUrls.length; i++) {
142
+ const result = results[i];
143
+ try {
144
+ if (result.status !== 'fulfilled') throw result.reason;
145
+ parsed.push(parseArtificialAnalysis(result.value.bytes, { ...result.value.source, identityBindings: bindings }));
146
+ } catch (error) { errors.push(`evaluations ${aaUrls[i - 1]}: ${error.message}`); }
147
+ }
148
+ // All requested pages must parse before certifying a new matrix; partial pages are evidence only.
149
+ if (parsed.length === aaUrls.length && parsed.length > 0) {
150
+ const records = new Map();
151
+ for (const page of parsed) for (const record of page.records) records.set(record.sourceModelId, record);
152
+ evaluations = { checkedAt, sources: parsed.map((p) => p.source), records: [...records.values()],
153
+ selectionQualified: false, limitation: 'Independent benchmark evidence; native access and supported effort require separate verification. Arena is not collected.' };
154
+ }
155
+ let official = prior.officialSources;
156
+ const publicDocs = [];
157
+ for (let i = 0; i < officialSources.length; i++) {
158
+ const result = results[1 + aaUrls.length + i];
159
+ if (result.status === 'fulfilled') publicDocs.push({ ...result.value.source, provider: officialSources[i].provider,
160
+ scope: 'official public/API documentation; not native subscription access', semanticallyQualified: false });
161
+ else errors.push(`official ${officialSources[i].url}: ${result.reason.message}`);
162
+ }
163
+ if (publicDocs.length === officialSources.length) official = { checkedAt, sources: publicDocs };
164
+ let agents = prior.agentSources;
165
+ if (agentSourceUrls.length) {
166
+ const agentResults = results.slice(1 + aaUrls.length + officialSources.length, 1 + aaUrls.length + officialSources.length + agentSourceUrls.length);
167
+ try {
168
+ if (agentResults.length !== 2) throw new Error('coding-agent source and methodology pair required');
169
+ for (const result of agentResults) if (result.status !== 'fulfilled') throw result.reason;
170
+ const parsedAgents = parseCodingAgentEvidence(agentResults[0].value.bytes, agentResults[1].value.bytes,
171
+ { ...agentResults[0].value.source, identityBindings: bindings });
172
+ agents = { checkedAt, sources: agentResults.map((r) => r.value.source), ...parsedAgents };
173
+ } catch (error) { errors.push(`coding agents: ${error.message}`); }
174
+ }
175
+ const additional = [];
176
+ for (let i = 0; i < codingBenchmarkSources.length; i++) {
177
+ const result = results[1 + aaUrls.length + officialSources.length + agentSourceUrls.length + i];
178
+ if (result.status === 'fulfilled' && result.value.bytes.trim().length >= 100) {
179
+ additional.push({ ...result.value.source, suite: codingBenchmarkSources[i].suite,
180
+ scope: 'raw source archive; independent suite, score parsing and semantic qualification not implemented' });
181
+ } else errors.push(`coding benchmark ${codingBenchmarkSources[i].url}: ${result.status === 'rejected' ? result.reason.message : 'empty or truncated source'}`);
182
+ }
183
+ if (additional.length === codingBenchmarkSources.length && agents && agents !== prior.agentSources) agents = { ...agents, additionalSources: additional };
184
+ else if (additional.length !== codingBenchmarkSources.length) agents = prior.agentSources;
185
+ const instructionPath = path.join(routerDir, 'weekly-analyst-instruction.md');
186
+ let instruction; let instructionSource = 'packaged-fallback';
187
+ try {
188
+ const fd = fs.openSync(instructionPath, 'r');
189
+ try {
190
+ const buffer = Buffer.alloc(128 * 1024 + 1);
191
+ const size = fs.readSync(fd, buffer, 0, buffer.length, 0);
192
+ if (size > 128 * 1024) throw new Error('effective weekly instruction exceeds 128 KiB');
193
+ instruction = buffer.subarray(0, size).toString('utf8');
194
+ if (!instruction.trim()) throw new Error('effective weekly instruction is empty');
195
+ instructionSource = 'effective-per-user-file';
196
+ } finally { fs.closeSync(fd); }
197
+ } catch (error) {
198
+ if (error.code !== 'ENOENT') { errors.push(`instruction: ${error.message}`); instruction = undefined; instructionSource = 'fallback-after-read-error'; }
199
+ }
200
+ const next = { schemaVersion: 1, maxAgeMs: WEEK_MS, inventory, evaluations, officialSources: official, agentSources: agents,
201
+ lastAttempt: { checkedAt, status: errors.length ? (inventory === prior.inventory && evaluations === prior.evaluations ? 'failed' : 'partial') : 'complete', errors } };
202
+ let priorPolicyBytes = null; let policy = null;
203
+ try { priorPolicyBytes = fs.readFileSync(path.join(routerDir, 'routing-policy.json'), 'utf8'); policy = JSON.parse(priorPolicyBytes); }
204
+ catch { /* no policy: report missing allocation, never synthesize one */ }
205
+ const assessment = buildWeeklyAssessment({ currency: next, policy, priorPolicyBytes, now, previousAssessment: prior.assessment, instruction, instructionSource });
206
+ const assessmentDir = path.join(routerDir, 'assessments', `${checkedAt.replaceAll(':', '-')}-${lock}`);
207
+ for (const [name, bytes] of [['report.json', JSON.stringify(assessment.report, null, 2)],
208
+ ['proposal.json', JSON.stringify(assessment.proposal, null, 2)], ['report.md', assessment.markdown],
209
+ ['instruction.md', assessment.instruction], ['prior-policy.json', priorPolicyBytes ?? 'null']]) {
210
+ fencedWrite(routerDir, lock, path.join(assessmentDir, name), bytes);
211
+ }
212
+ if (!fs.existsSync(instructionPath)) fencedWrite(routerDir, lock, instructionPath, assessment.instruction);
213
+ next.assessment = { instructionPath, instructionSha256: assessment.report.instructionSha256, instructionSource, checkedAt, signature: assessment.report.signature, reportPath: path.join(assessmentDir, 'report.json'),
214
+ proposalPath: path.join(assessmentDir, 'proposal.json'), status: 'complete-unqualified', analystExecuted: false,
215
+ notification: assessment.report.notification };
216
+ fencedWrite(routerDir, lock, path.join(routerDir, 'currency.json'), `${JSON.stringify(next, null, 2)}\n`);
217
+ return { action: 'refreshed', ...currencyStatus(next, now), lastAttempt: next.lastAttempt };
218
+ } finally { release(routerDir, lock); }
219
+ }
220
+
221
+ if (process.argv[1] && path.resolve(process.argv[1]) === SELF) {
222
+ const dirIndex = process.argv.indexOf('--router-dir');
223
+ const routerDir = dirIndex >= 0 ? process.argv[dirIndex + 1] : DEFAULT_ROUTER_DIR;
224
+ if (!routerDir || !path.isAbsolute(routerDir)) throw new Error('--router-dir must be absolute');
225
+ if (process.argv.includes('--refresh')) {
226
+ refreshModelCurrency({ routerDir, claimToken: process.argv.includes('--claim-token') ? process.argv[process.argv.indexOf('--claim-token') + 1] : null }).then((result) => {
227
+ console.log(JSON.stringify(result)); if (result.status === 'stale') process.exitCode = 1;
228
+ }).catch((error) => { console.error(error.message); process.exitCode = 1; });
229
+ } else console.log(JSON.stringify(process.argv.includes('--catch-up') ? maybeLaunchCurrencyRefresh({ routerDir }) : readCurrencyStatus({ routerDir })));
230
+ }
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // DISTINCT-FROM: model-router-catalog.mjs — fresh native subscription metadata, never model execution or policy promotion.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { fileURLToPath } from 'node:url';
8
+ const HOSTS = { codex: 'openai', 'claude-code': 'anthropic' };
9
+ const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
10
+ const read = (file, optional = false) => {
11
+ try { const bytes = fs.readFileSync(file); if (bytes.length > 1048576) throw new Error('Native catalog input exceeds limit'); return bytes; }
12
+ catch (error) { if (optional && error.code === 'ENOENT') return null; throw error; }
13
+ };
14
+ function stage(file, bytes) {
15
+ const temporary = `${file}.${randomUUID()}.tmp`; const fd = fs.openSync(temporary, 'wx', 0o600);
16
+ try { fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); } catch (error) { fs.closeSync(fd); fs.unlinkSync(temporary); throw error; }
17
+ fs.closeSync(fd);
18
+ return temporary;
19
+ }
20
+ const equal = (a, b) => a === null ? b === null : b !== null && a.equals(b);
21
+ export async function refreshNativeCatalog({ routerDir = path.join(os.homedir(), '.claude', 'model-router'),
22
+ deadline = Date.now() + 60000, inspect, now = Date.now() } = {}) {
23
+ const checkedAt = new Date(now).toISOString();
24
+ const failed = reason => ({ status: 'failed', checkedAt, newNativeModelIds: [], policyApplied: false, reason });
25
+ const paths = { profile: path.join(routerDir, 'profile.json'), catalog: path.join(routerDir, 'catalog.json'),
26
+ availability: path.join(routerDir, 'native-availability.json') };
27
+ let guard; let acquired = false; const temporary = [];
28
+ try {
29
+ if (!path.isAbsolute(routerDir) || !Number.isFinite(deadline) || deadline <= Date.now()) return failed('Invalid native metadata directory or deadline');
30
+ const before = Object.fromEntries(Object.entries(paths).map(([key, file]) => [key, read(file, key === 'availability')]));
31
+ const profile = JSON.parse(before.profile); const catalog = JSON.parse(before.catalog);
32
+ if (profile.automaticModelRoutingUpdates !== true) return failed('Automatic native model registration is not authorized by this profile');
33
+ if (!Array.isArray(catalog.candidates) || catalog.candidates.some(c => typeof c.id !== 'string')
34
+ || new Set(catalog.candidates.map(c => c.id)).size !== catalog.candidates.length) return failed('Existing model catalog is malformed');
35
+ const hosts = Object.keys(HOSTS).filter(host => profile.harnesses?.[host]?.available === true
36
+ && profile.harnesses[host].subscription === true && profile.harnesses[host].enabled !== false);
37
+ if (!hosts.length) return failed('No enabled native subscription host in this profile');
38
+ inspect ??= (await import('./model-native-qualification.mjs')).inspectNativeQualificationHost;
39
+ const observations = []; const eligibleIds = []; const nativeIds = []; const unknownEffortIds = []; const withheldIds = [];
40
+ const candidates = [...catalog.candidates];
41
+ for (const host of hosts) {
42
+ if (Date.now() >= deadline) return failed('Native metadata deadline expired');
43
+ const result = await inspect({ host, deadline });
44
+ if (result?.failure || result?.available !== true || result.nativeSubscription !== true || result.sourceReceipt?.host !== host
45
+ || result.sourceReceipt.allowance?.verified !== true || result.catalogStatus !== 'verified'
46
+ || !Array.isArray(result.models) || !result.models.length) return failed(`Verified native model list unavailable for ${host}`);
47
+ const models = [];
48
+ for (const model of result.models) {
49
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]+$/.test(model.id ?? '')) return failed(`Exact native model identity unavailable for ${host}`);
50
+ const id = `${HOSTS[host]}/${model.id}`; nativeIds.push(id);
51
+ const nativeEfforts = Array.isArray(model.efforts) ? model.efforts.filter(e => typeof e === 'string' && /^[a-z][a-z0-9_-]{0,31}$/.test(e)) : [];
52
+ const efforts = [...new Set(nativeEfforts.filter(e => EFFORTS.has(e)))].sort();
53
+ const unrecognizedEfforts = [...new Set(nativeEfforts.filter(e => !EFFORTS.has(e)))].sort();
54
+ const knownEfforts = efforts.length > 0;
55
+ models.push({ id: model.id, efforts, effortSupport: knownEfforts ? 'verified' : 'unknown',
56
+ ...(unrecognizedEfforts.length ? { unrecognizedEfforts } : {}) });
57
+ if (!knownEfforts) { unknownEffortIds.push(id); continue; }
58
+ const existing = candidates.find(c => c.id === model.id);
59
+ if (existing) {
60
+ if (existing.provider !== HOSTS[host] || existing.disabled === true || existing.enabled === false
61
+ || !existing.harness?.includes(host) || !existing.subscription?.includes(host)) { withheldIds.push(id); continue; }
62
+ } else candidates.push({ id: model.id, provider: HOSTS[host], harness: [host], subscription: [host],
63
+ supportedEfforts: efforts, costPerMTok: null, verified: `${checkedAt} native subscription metadata; execution not tested`,
64
+ note: 'Native catalog availability is not role-quality qualification or active routing approval.' });
65
+ eligibleIds.push(id);
66
+ }
67
+ if (new Set(models.map(m => m.id)).size !== models.length) return failed(`Duplicate native model identity for ${host}`);
68
+ observations.push({ host, provider: HOSTS[host], checkedAt: result.checkedAt, harnessVersion: result.harnessVersion, allowanceVerified: true, models: models.sort((a, b) => a.id.localeCompare(b.id)) });
69
+ }
70
+ if (Date.now() >= deadline) return failed('Native metadata deadline expired');
71
+ const previous = before.availability ? JSON.parse(before.availability) : null;
72
+ if (previous && (previous.schemaVersion !== 1 || !Array.isArray(previous.nativeModelIds)
73
+ || previous.nativeModelIds.some(id => typeof id !== 'string'))) return failed('Previous native availability baseline is malformed');
74
+ const ids = [...new Set(eligibleIds)].sort(); const nativeModelIds = [...new Set(nativeIds)].sort();
75
+ const newNativeModelIds = previous ? nativeModelIds.filter(id => !previous.nativeModelIds.includes(id)) : [];
76
+ const snapshot = { schemaVersion: 1, status: 'current', checkedAt, hosts: observations,
77
+ nativeModelIds, eligibleModelIds: ids, unknownEffortModelIds: unknownEffortIds.sort(), withheldExistingModelIds: [...new Set(withheldIds)].sort(), inferenceRequests: 0, policyApplied: false };
78
+ const changedCatalog = candidates.length !== catalog.candidates.length;
79
+ const catalogBytes = changedCatalog ? Buffer.from(JSON.stringify({ ...catalog, candidates }, null, 2) + '\n') : before.catalog;
80
+ const snapshotBytes = Buffer.from(JSON.stringify(snapshot, null, 2) + '\n');
81
+ guard = path.join(routerDir, 'native-catalog-mutation.lock'); fs.mkdirSync(guard, { mode: 0o700 }); acquired = true;
82
+ for (const [key, file] of Object.entries(paths)) if (!equal(before[key], read(file, key === 'availability'))) throw new Error('Native catalog inputs changed');
83
+ const history = path.join(routerDir, 'native-catalog-history', `${now}-${randomUUID()}`); fs.mkdirSync(history, { recursive: true, mode: 0o700 });
84
+ for (const [name, bytes] of [['catalog.json', before.catalog], ['native-availability.json', before.availability]]) {
85
+ if (!bytes) continue; const target = path.join(history, name); const temp = stage(target, bytes); temporary.push(temp); fs.renameSync(temp, target);
86
+ }
87
+ const catalogTemp = changedCatalog ? stage(paths.catalog, catalogBytes) : null;
88
+ if (catalogTemp) temporary.push(catalogTemp);
89
+ const snapshotTemp = stage(paths.availability, snapshotBytes); temporary.push(snapshotTemp);
90
+ if (catalogTemp) fs.renameSync(catalogTemp, paths.catalog);
91
+ try { fs.renameSync(snapshotTemp, paths.availability); }
92
+ catch (error) {
93
+ if (catalogTemp && equal(catalogBytes, read(paths.catalog))) {
94
+ const restore = stage(paths.catalog, before.catalog); temporary.push(restore); fs.renameSync(restore, paths.catalog);
95
+ }
96
+ throw error;
97
+ }
98
+ return { status: 'current', checkedAt, newNativeModelIds, baseline: previous === null,
99
+ catalogAdded: candidates.length - catalog.candidates.length, unknownEffortModelIds: snapshot.unknownEffortModelIds, withheldExistingModelIds: snapshot.withheldExistingModelIds,
100
+ inferenceRequests: 0, policyApplied: false };
101
+ } catch { return failed('Native catalog refresh refused; previous availability and owner catalog retained where unchanged'); }
102
+ finally {
103
+ for (const file of temporary) { try { fs.unlinkSync(file); } catch { /* renamed */ } }
104
+ if (acquired) { try { fs.rmdirSync(guard); } catch { /* not acquired */ } }
105
+ }
106
+ }
107
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
108
+ const option = name => { const at = process.argv.indexOf(name); return at < 0 ? undefined : process.argv[at + 1]; };
109
+ refreshNativeCatalog({ routerDir: option('--router-dir'), deadline: option('--deadline') === undefined ? undefined : Number(option('--deadline')) })
110
+ .then(result => { console.log(JSON.stringify(result)); if (result.status !== 'current') process.exitCode = 1; });
111
+ }
@@ -0,0 +1,251 @@
1
+ // Native subscription qualification: fixed plaintext tasks, never candidate code execution.
2
+ // Protocol sequence derives from the source-bound native gateway acceptance probe.
3
+ // Configuration identity is observable; provider/backend identity is never proved here.
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import crypto from 'node:crypto';
8
+ import { spawn, execFileSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { StringDecoder } from 'node:string_decoder';
11
+ import { assertSubscriptionAuth, subscriptionEnvironment } from './model-router-dispatch.mjs';
12
+ import { discoverNativeCodex } from './model-routing-launchers.mjs';
13
+
14
+ const SELF = fileURLToPath(import.meta.url);
15
+ const digest = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
16
+ const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
17
+ const ID = /^[a-z][a-z0-9]*(?:[-.][a-z0-9]+)*$/;
18
+ const redact = (key, value) => /^(?:email|account_id|access_token|refresh_token|id_token|api_key|authorization|credentials)$/i.test(key) ? '[redacted]' : value;
19
+
20
+ function binaryFor(host) {
21
+ const extensions = path.join(os.homedir(), '.vscode-server/extensions');
22
+ if (host === 'codex') return discoverNativeCodex(extensions);
23
+ const entries = JSON.parse(fs.readFileSync(path.join(extensions, 'extensions.json')));
24
+ const entry = entries.find((e) => e.identifier?.id === 'anthropic.claude-code');
25
+ if (!entry) throw new Error('Native Claude extension unavailable');
26
+ return path.join(entry.location.path, 'resources/native-binary', process.platform === 'win32' ? 'claude.exe' : 'claude');
27
+ }
28
+
29
+ export function verifyClaudeAllowance(usage, model) {
30
+ // Native 2.1.289 get_usage is explicitly experimental. Unknown shapes fail closed.
31
+ if (usage?.rate_limits_available !== true || usage.rate_limits?.extra_usage?.is_enabled !== false) throw new Error('Native Claude allowance unknown or extra usage enabled');
32
+ const limits = usage.rate_limits;
33
+ for (const key of ['five_hour', 'seven_day']) {
34
+ const window = limits[key];
35
+ if (!window || !Number.isFinite(window.utilization) || window.utilization < 0 || window.utilization >= 100 || window.locked_reason !== null) throw new Error('Native Claude ordinary allowance unavailable');
36
+ }
37
+ for (const scoped of limits.model_scoped || []) {
38
+ if (!Number.isFinite(scoped.utilization) || scoped.utilization < 0 || scoped.utilization >= 100 || scoped.locked_reason != null) throw new Error('Native Claude scoped allowance unavailable or unmapped');
39
+ }
40
+ for (const limit of limits.limits || []) {
41
+ if (!Number.isFinite(limit.percent) || limit.percent < 0 || limit.percent >= 100 || !['normal', 'warning'].includes(limit.severity)) throw new Error('Native Claude allowance unavailable or unknown');
42
+ }
43
+ return { verified: true, basis: 'native-get_usage-2.1.289', checkedAt: new Date().toISOString(), reservation: false, raceSafe: false, model };
44
+ }
45
+
46
+ class Transport {
47
+ constructor(child, host, fail, transcript, signal) {
48
+ this.child = child; this.host = host; this.fail = fail; this.transcript = transcript;
49
+ this.pending = new Map(); this.events = []; this.waiters = []; this.serial = 0; this.buffer = ''; this.bytes = 0; this.decoder = new StringDecoder('utf8');
50
+ child.stderr.on('data', () => {}); child.stdin.on('error', () => fail(new Error('Native input transport failed')));
51
+ child.once('error', () => fail(new Error('Native executable unavailable')));
52
+ child.once('exit', () => { if (!signal.aborted) fail(new Error('Native host exited before qualification')); });
53
+ child.stdout.on('data', (chunk) => {
54
+ this.bytes += chunk.length; if (this.bytes > 512 * 1024) return fail(new Error('Native transcript exceeded bound'));
55
+ this.buffer += this.decoder.write(chunk);
56
+ let at;
57
+ while ((at = this.buffer.indexOf('\n')) >= 0) {
58
+ const line = this.buffer.slice(0, at); this.buffer = this.buffer.slice(at + 1);
59
+ let message; try { message = JSON.parse(line); } catch { return fail(new Error('Malformed native protocol output')); }
60
+ transcript.push({ direction: 'native', packet: message });
61
+ const id = host === 'codex' ? message.id : message.type === 'control_response' ? message.response?.request_id : null;
62
+ const pending = this.pending.get(id);
63
+ if (pending) {
64
+ this.pending.delete(id);
65
+ if (message.error || (host === 'claude-code' && message.response.subtype !== 'success')) pending.reject(new Error('Native request refused'));
66
+ else pending.resolve(host === 'codex' ? message.result : message.response.response);
67
+ } else {
68
+ // No native tool/approval request is ever serviced by this adapter.
69
+ const tool = host === 'codex' ? Object.hasOwn(message, 'id') && Boolean(message.method)
70
+ : message.type === 'control_request' || message.message?.content?.some((p) => p.type === 'tool_use');
71
+ const item = message.params?.item;
72
+ if (tool || (item?.type && !['agentMessage', 'reasoning', 'userMessage'].includes(item.type))) return fail(new Error('Native tool inference refused'));
73
+ this.events.push(message);
74
+ for (const waiter of [...this.waiters]) if (waiter.predicate(message)) { this.waiters.splice(this.waiters.indexOf(waiter), 1); waiter.resolve(message); }
75
+ }
76
+ }
77
+ });
78
+ }
79
+ send(packet) { this.transcript.push({ direction: 'client', packet }); this.child.stdin.write(`${JSON.stringify(packet)}\n`); }
80
+ request(method, params = {}) {
81
+ const id = `qualification-${++this.serial}`;
82
+ const promise = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
83
+ this.send(this.host === 'codex' ? { id, method, params } : { type: 'control_request', request_id: id, request: { subtype: method, ...params } });
84
+ return promise;
85
+ }
86
+ wait(predicate, start = 0) {
87
+ const existing = this.events.slice(start).find(predicate); if (existing) return Promise.resolve(existing);
88
+ return new Promise((resolve) => this.waiters.push({ predicate, resolve }));
89
+ }
90
+ }
91
+
92
+ async function run(options, inspectOnly) {
93
+ const { host, model, effort, prompt, cwd, env = process.env, deadline = Date.now() + 60000,
94
+ spawnHost = spawn, versionProbe = execFileSync, authCheck = assertSubscriptionAuth, now = Date.now } = options;
95
+ const started = now(); const transcript = []; let child, transport;
96
+ const result = { completed: false, output: '', nativeModel: null, nativeEffort: null, identityBasis: null,
97
+ backendIdentityProved: false, nativeSubscription: false, available: false, supported: false, models: [],
98
+ harnessVersion: null, elapsedMs: 0, nativeSessionId: null, nativeTurnId: null, sourceReceipt: {
99
+ schemaVersion: 1, kind: 'native-model-qualification', host, backendIdentityProved: false,
100
+ adapterSha256: digest(fs.readFileSync(SELF)), nativeSettings: {}, nativeTurn: null,
101
+ request: inspectOnly ? null : { model, effort, promptSha256: digest(String(prompt)) }, transcriptRedactions: ['account/auth identifying fields'],
102
+ } };
103
+ const controller = new AbortController(); let abortError;
104
+ const fail = (error) => { if (!controller.signal.aborted) { abortError = error; controller.abort(); } };
105
+ const remaining = () => { const ms = deadline - now(); if (ms <= 0) throw new Error('Shared native qualification deadline exhausted'); return ms; };
106
+ const bounded = (promise) => new Promise((resolve, reject) => {
107
+ if (controller.signal.aborted) return reject(abortError);
108
+ const stop = () => reject(abortError); controller.signal.addEventListener('abort', stop, { once: true });
109
+ Promise.resolve(promise).then(resolve, reject).finally(() => controller.signal.removeEventListener('abort', stop));
110
+ });
111
+ let timer;
112
+ try {
113
+ if (!['codex', 'claude-code'].includes(host) || !Number.isFinite(deadline)) throw new Error('Supported native host and absolute deadline required');
114
+ remaining(); timer = setTimeout(() => fail(new Error('Shared native qualification deadline exhausted')), remaining());
115
+ if (!inspectOnly && (!ID.test(model || '') || !EFFORTS.has(effort) || typeof prompt !== 'string' || !prompt.trim() || Buffer.byteLength(prompt) > 16384 || !path.isAbsolute(cwd || ''))) throw new Error('Static model/effort, bounded plaintext prompt and absolute sandbox cwd required');
116
+ const binary = fs.realpathSync(options.binary || binaryFor(host));
117
+ result.sourceReceipt.binary = binary;
118
+ // Exact native executable bytes, without retaining executable or credentials in the receipt.
119
+ const hash = crypto.createHash('sha256'); const stream = fs.createReadStream(binary);
120
+ const stopHash = () => stream.destroy(abortError);
121
+ controller.signal.addEventListener('abort', stopHash, { once: true });
122
+ try { await bounded(new Promise((resolve, reject) => { stream.on('data', (bytes) => { try { remaining(); hash.update(bytes); } catch (e) { stream.destroy(e); } }); stream.once('error', reject); stream.once('end', resolve); })); }
123
+ finally { controller.signal.removeEventListener('abort', stopHash); }
124
+ result.sourceReceipt.nativeBinarySha256 = hash.digest('hex');
125
+ const clean = subscriptionEnvironment(env);
126
+ result.harnessVersion = String(versionProbe(binary, ['--version'], { env: clean, encoding: 'utf8', timeout: remaining(), stdio: ['ignore', 'pipe', 'pipe'] })).trim().slice(0, 128);
127
+ await bounded(authCheck(host, { env: clean, probe: (_command, args, opts) => versionProbe(binary, args, { ...opts, timeout: remaining() }) }));
128
+ result.nativeSubscription = true;
129
+ const args = host === 'codex' ? ['app-server', '--strict-config', '-c', 'model_provider="openai"', '-c', 'service_tier="default"', '-c', 'features.fast_mode=false',
130
+ '-c', 'features.shell_tool=false', '-c', 'features.unified_exec=false', '-c', 'features.code_mode=false', '-c', 'features.code_mode_host=false', '-c', 'features.multi_agent=false', '-c', 'features.plugins=false', '-c', 'web_search="disabled"', '-c', 'mcp_servers={}']
131
+ : ['--print', '--verbose', '--input-format', 'stream-json', '--output-format', 'stream-json', '--include-partial-messages', '--tools', '', '--mcp-config', '{"mcpServers":{}}', '--strict-mcp-config', '--no-session-persistence'];
132
+ remaining(); child = spawnHost(binary, args, { env: clean, cwd: cwd || os.tmpdir(), shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
133
+ transport = new Transport(child, host, fail, transcript, controller.signal);
134
+ const metadata = (promise) => {
135
+ const timeout = setTimeout(() => fail(new Error(`Native metadata timeout: ${result.sourceReceipt.stage}`)), Math.min(10000, remaining()));
136
+ return bounded(promise).finally(() => clearTimeout(timeout));
137
+ };
138
+ const request = (...params) => { result.sourceReceipt.stage = params[0]; return metadata(transport.request(...params)); };
139
+ if (host === 'codex') {
140
+ await request('initialize', { clientInfo: { name: 'native_model_qualification', version: '1' }, capabilities: { experimentalApi: true } }); transport.send({ method: 'initialized' });
141
+ const account = await request('account/read', { refreshToken: false });
142
+ if (account?.account?.type !== 'chatgpt') throw new Error('Native same-process subscription account unavailable');
143
+ const allowance = await request('account/rateLimits/read', { excludeResetCreditDetails: true, supportsLunaReserve: false });
144
+ if (allowance?.ordinaryUsageAllowed !== true) throw new Error('Native ordinary subscription allowance unavailable');
145
+ result.sourceReceipt.allowance = { verified: true, basis: 'native-account/rateLimits/read', reservation: false, raceSafe: false };
146
+ let cursor;
147
+ do {
148
+ const catalog = await request('model/list', { ...(cursor ? { cursor } : {}), includeHidden: true });
149
+ if (!Array.isArray(catalog?.data)) throw new Error('Native model catalog unavailable');
150
+ result.models.push(...catalog.data.map((m) => ({ id: m.model, efforts: (m.supportedReasoningEfforts || []).map((e) => e.reasoningEffort), inputModalities: m.inputModalities })));
151
+ cursor = catalog.nextCursor;
152
+ if (result.models.length > 1000) throw new Error('Native catalog exceeded bound');
153
+ } while (cursor);
154
+ result.available = true; result.catalogStatus = 'verified';
155
+ if (!inspectOnly) {
156
+ if (!result.models.some((m) => m.id === model && m.efforts.includes(effort))) throw new Error('Native model/effort unsupported');
157
+ result.supported = true;
158
+ const thread = await request('thread/start', { cwd, model, modelProvider: 'openai', serviceTier: 'default', sandbox: 'read-only', approvalPolicy: 'untrusted', ephemeral: true, environments: [], dynamicTools: [],
159
+ developerInstructions: 'Qualification: plaintext answer only. Never use tools, read files, execute code, or delegate.' });
160
+ const threadId = thread?.thread?.id; if (!threadId) throw new Error('Native thread identity missing');
161
+ result.nativeSessionId = threadId;
162
+ const settings = async (params) => {
163
+ const start = transport.events.length;
164
+ await request('thread/settings/update', { threadId, ...params });
165
+ result.sourceReceipt.stage = 'thread/settings/updated';
166
+ const event = await metadata(transport.wait((m) => m.method === 'thread/settings/updated' && m.params?.threadId === threadId, start));
167
+ const s = event.params.threadSettings;
168
+ if (s?.model !== model || s.effort !== effort || s.modelProvider !== 'openai' || s.serviceTier !== 'default') throw new Error('Native configured model/effort/provider mismatch');
169
+ return { model: s.model, effort: s.effort, provider: s.modelProvider, serviceTier: s.serviceTier, threadId };
170
+ };
171
+ result.sourceReceipt.nativeSettings.before = await settings({ model, effort, serviceTier: 'default' });
172
+ const accepted = await request('turn/start', { threadId, model, effort, serviceTier: 'default', input: [{ type: 'text', text: prompt }] });
173
+ const turnId = accepted?.turn?.id; if (!turnId) throw new Error('Native turn identity missing');
174
+ result.sourceReceipt.stage = 'turn/completed';
175
+ const end = await bounded(transport.wait((m) => m.method === 'turn/completed' && m.params?.threadId === threadId && m.params?.turn?.id === turnId));
176
+ if (end.params.turn.status !== 'completed') throw new Error('Native turn did not complete');
177
+ const items = transport.events.filter((m) => m.method === 'item/completed' && m.params?.threadId === threadId && m.params?.turnId === turnId);
178
+ result.output = items.filter((m) => m.params.item?.type === 'agentMessage').map((m) => m.params.item.text || '').join('\n');
179
+ result.nativeTurnId = turnId;
180
+ // Native same-value updates emit no notification. Read actual live thread
181
+ // metadata after completion; never infer settings from a no-op acknowledgment.
182
+ const observed = (await request('thread/read', { threadId, includeTurns: false }))?.thread;
183
+ if (observed?.id !== threadId || observed.model !== model || observed.reasoningEffort !== effort || observed.modelProvider !== 'openai') throw new Error('Native completed thread model/effort/provider mismatch');
184
+ result.sourceReceipt.nativeSettings.after = { model: observed.model, effort: observed.reasoningEffort, provider: observed.modelProvider, threadId, basis: 'native-thread/read' };
185
+ if (Object.hasOwn(observed, 'serviceTier')) {
186
+ if (observed.serviceTier !== 'default') throw new Error('Native completed thread service tier mismatch');
187
+ result.sourceReceipt.nativeSettings.after.serviceTier = observed.serviceTier;
188
+ result.sourceReceipt.nativeSettings.after.serviceTierBasis = 'native-thread/read';
189
+ } else result.sourceReceipt.nativeSettings.after.serviceTierBasis = 'pre-turn-native-settings-and-fixed-host-configuration';
190
+ }
191
+ } else {
192
+ if (!/^2\.1\.289 \(Claude Code\)$/.test(result.harnessVersion)) throw new Error('Experimental Claude allowance contract version unsupported');
193
+ const initialized = await request('initialize');
194
+ if (initialized?.session_state !== 'idle') throw new Error('Native Claude session not idle');
195
+ result.sourceReceipt.allowance = verifyClaudeAllowance(await request('get_usage', { skip_behaviors: true }), model);
196
+ // Native initialize.models is the extension's supportedModels() source.
197
+ result.catalogStatus = Array.isArray(initialized.models) ? 'verified' : 'unknown';
198
+ for (const m of initialized.models || []) {
199
+ if (typeof m.resolvedModel !== 'string') throw new Error('Native Claude model catalog malformed');
200
+ if (!result.models.some((entry) => entry.id === m.resolvedModel)) result.models.push({ id: m.resolvedModel,
201
+ efforts: m.supportsEffort === true && Array.isArray(m.supportedEffortLevels) ? m.supportedEffortLevels.filter((e) => EFFORTS.has(e)) : [] });
202
+ }
203
+ result.available = true;
204
+ if (!inspectOnly) {
205
+ if (!result.models.some((m) => m.id === model && m.efforts.includes(effort))) throw new Error('Native Claude model/effort unsupported or unknown');
206
+ await request('apply_flag_settings', { settings: { model, effortLevel: effort } });
207
+ const settings = async () => {
208
+ const observed = (await request('get_settings'))?.applied;
209
+ if (observed?.model !== model || observed.effort !== effort) throw new Error('Native Claude configured model/effort mismatch');
210
+ return { model: observed.model, effort: observed.effort };
211
+ };
212
+ result.sourceReceipt.nativeSettings.before = await settings(); result.supported = true;
213
+ transport.send({ type: 'user', uuid: crypto.randomUUID(), session_id: '', message: { role: 'user', content: [{ type: 'text', text: prompt }] }, parent_tool_use_id: null });
214
+ result.sourceReceipt.stage = 'result';
215
+ const completion = await bounded(transport.wait((m) => m.type === 'result'));
216
+ if (completion.is_error || completion.subtype !== 'success' || !completion.uuid || !completion.session_id || !completion.usage || !(completion.duration_api_ms > 0) || !(completion.num_turns > 0)) throw new Error('Native Claude completion identity/usage unavailable');
217
+ result.output = completion.result || ''; result.nativeSessionId = completion.session_id; result.nativeTurnId = completion.uuid;
218
+ result.sourceReceipt.nativeSettings.after = await settings();
219
+ }
220
+ }
221
+ if (!inspectOnly) {
222
+ if (typeof result.output !== 'string' || !result.output.trim() || Buffer.byteLength(result.output) > 32768) throw new Error('Native plaintext output missing or exceeded bound');
223
+ result.completed = true; result.nativeModel = result.sourceReceipt.nativeSettings.after.model; result.nativeEffort = result.sourceReceipt.nativeSettings.after.effort; result.identityBasis = 'native-host-confirmed-configuration';
224
+ result.sourceReceipt.nativeTurn = { threadId: result.nativeSessionId, turnId: result.nativeTurnId, status: 'completed', toolEvents: [],
225
+ turnIdBasis: host === 'codex' ? 'native-turn.id' : 'native-result.uuid' };
226
+ result.sourceReceipt.stage = 'completed';
227
+ }
228
+ } catch (error) { result.failure = error.message; result.completed = false; result.output = ''; }
229
+ finally {
230
+ clearTimeout(timer); controller.abort();
231
+ if (child) {
232
+ await new Promise((resolve) => {
233
+ const grace = setTimeout(() => { child.kill('SIGKILL'); resolve(); }, Math.max(0, Math.min(100, deadline - now())));
234
+ child.once('close', () => { clearTimeout(grace); resolve(); });
235
+ child.stdin.end(); child.kill('SIGTERM');
236
+ });
237
+ }
238
+ result.elapsedMs = Math.max(0, now() - started); result.checkedAt = new Date().toISOString();
239
+ result.transcript = transcript.map((frame) => JSON.stringify(frame, redact)).join('\n');
240
+ if (Buffer.byteLength(result.transcript) > 1024 * 1024) {
241
+ result.completed = false; result.output = ''; result.failure = 'Archived native transcript exceeded bound';
242
+ result.transcript = ''; result.sourceReceipt.nativeTurn = null;
243
+ }
244
+ result.transcriptSha256 = digest(result.transcript); result.sourceReceipt.transcriptSha256 = result.transcriptSha256;
245
+ result.sourceReceipt.harnessVersion = result.harnessVersion;
246
+ }
247
+ return result;
248
+ }
249
+
250
+ export const runNativeQualification = (options) => run(options, false);
251
+ export const inspectNativeQualificationHost = (options) => run(options, true);