From df481f3f345c71a5c462a01c17502d1c040061de Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:22:38 +0200 Subject: [PATCH 01/39] fix(code): stop retrying cleanup-confirmed execution budgets --- docs/OPENCODE.md | 15 ++++- scripts/opencode_session.cjs | 4 +- scripts/smoke_opencode_inference.py | 6 +- src/chat-completions-provider.cjs | 9 ++- src/opencode-bridge.cjs | 7 ++- src/private-compute.cjs | 9 ++- src/private-conversation.cjs | 20 +++++-- tests/chat-completions-provider.test.cjs | 39 ++++++++++++- tests/opencode-runtime.test.cjs | 19 +++++- tests/opencode-session.test.cjs | 10 ++-- tests/private-compute.test.cjs | 16 ++++- tests/private-conversation.test.cjs | 74 ++++++++++++++++++++++++ tests/real_opencode_provider_errors.cjs | 20 ++++--- tests/test_smoke_opencode_inference.py | 7 ++- 14 files changed, 219 insertions(+), 36 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 7b225c9..e652534 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -75,6 +75,11 @@ admission. Model quality and useful execution still require real-model evidence. Before starting OpenCode, the owner validates the complete core capability reply, including template, limits, private-local scope and negotiated `greedy_v1` policy. +Current native sessions also require `execution_error_version: 1`; an older core +without that capability is refused before starting the runtime. Both native smoke +profiles pin core `08f23e11ce883f546262ec424940400283d58f22` for this contract. +Legacy Q&A and conversation clients that do not opt in remain unchanged. + That one model identity binds the native catalog, all agent roles, provider, task/session checks and trial receipt. Each provider request checks the core again; a different profile or incompatible result is refused, not silently substituted. @@ -112,13 +117,17 @@ for core cleanup before returning SDK-compatible SSE or JSON; this is not token-by-token model streaming. Exhaustion remains `length`, not successful `stop`. Cleanup-confirmed invalid/truncated output or an unmet tool choice returns a terminal HTTP 422, so the pinned SDK and OpenCode do not blindly regenerate that -unusable turn. Busy/execution/transport availability and uncertain cleanup remain -separate failures; this change does not make an incomplete answer usable. +unusable turn. The negotiated `execution_budget_exceeded` response also maps to +422, but only for the matching admitted task after confirmed core cleanup. It +does not claim a completed model response. Busy, other execution/transport +failures and uncertain cleanup remain separate; this change does not make an +incomplete answer usable. An already reported, known terminal task failure is separate from runtime cleanup: the task still fails, while a confirmed session/provider/process shutdown can succeed. Unknown/protocol errors and any unconfirmed cleanup still fail closed. The provider also counts cleanup for a correlated, admitted task ending with -the core's terminal `execution_failed` or `cancelled` response, or a valid result +the core's terminal `execution_failed`, `cancelled` or negotiated +`execution_budget_exceeded` response, or a valid result that races local cancellation. These remain failed requests, not model results. The receipt is retained per rejection inside the checked transport; an error code alone, admission alone, a cancellation acknowledgement or a disconnect cannot diff --git a/scripts/opencode_session.cjs b/scripts/opencode_session.cjs index 1bb3fbe..35c974e 100644 --- a/scripts/opencode_session.cjs +++ b/scripts/opencode_session.cjs @@ -135,11 +135,11 @@ async function runSession({input, output, events = process}, hooks = {}) { for (const name of ['SIGTERM', 'SIGINT', 'SIGHUP']) events.once(name, broken); try { const caps = capabilities(await (hooks.preflight ?? (async () => { - const core = new PrivateConversation('/opt/core/compute.sock', {generationPolicyVersion: 1}); + const core = new PrivateConversation('/opt/core/compute.sock', {generationPolicyVersion: 1, executionErrorVersion: 1}); try { return await core.connect(); } finally { core.close(); } - }))(), 1); + }))(), 1, 1); if (!isCodingModel(caps.model_profile) || !caps.native_tool_template || !caps.local_only || caps.quarantined || !caps.generation_policies.includes('greedy_v1')) throw Error('capabilities'); // The owner's already selected core determines this session's one model. diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index d9ff175..440e7d5 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -27,12 +27,12 @@ import uuid ROOT = Path(__file__).resolve().parents[1] -CORE = '845cc84d0d0b766ab1c5227231dbf6c8eaeb8cc3' +CORE = '08f23e11ce883f546262ec424940400283d58f22' MODEL = 'qwen3-0.6b-v1' GIB = 1024 ** 3 LARGE_MODEL = 'qwen3-4b-instruct-2507-v1' -# Separate reviewed source; the default profile retains its original core pin. -LARGE_CORE = '1297f8f1a5d163d802efd066c51a950b95588fa5' +# Both profiles require the reviewed terminal-budget IPC; model and budgets stay distinct. +LARGE_CORE = '08f23e11ce883f546262ec424940400283d58f22' MODEL_PROFILES = (MODEL, LARGE_MODEL) ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/src/chat-completions-provider.cjs b/src/chat-completions-provider.cjs index 0095575..84fe361 100644 --- a/src/chat-completions-provider.cjs +++ b/src/chat-completions-provider.cjs @@ -241,7 +241,7 @@ async function startChatCompletionsProvider({ socketPath, model, diagnostics = f if (closing || active) { errorReply(response, 503, 'busy'); return; } if (Number(request.headers['content-length'] ?? 0) > HTTP_BYTES) { errorReply(response, 413, 'request_bound'); return; } const controller = new AbortController(); - const client = new PrivateConversation(socketPath, { generationPolicyVersion: 1 }); + const client = new PrivateConversation(socketPath, { generationPolicyVersion: 1, executionErrorVersion: 1 }); const owner = { controller, client, done: null }; active = owner; response.once('close', () => { if (!response.writableFinished) controller.abort(); }); @@ -287,11 +287,14 @@ async function startChatCompletionsProvider({ socketPath, model, diagnostics = f } const code = error.message?.startsWith('private_compute_') ? error.code : 'provider_failed'; if (diagnostics) count(summary.request_errors, PROVIDER_ERRORS.includes(code) ? code : 'other'); - // These two errors follow a terminal, cleanup-confirmed model result. + // Output errors follow a terminal, cleanup-confirmed model result. The + // negotiated execution budget error instead follows confirmed cleanup + // without a model result; it must not restart the same expensive turn. // OpenCode v1.18.34 retries every 5xx, so 502 would repeatedly regenerate // the same unusable greedy turn. Preserve transient/uncertain failures. const status = ['busy', 'cleanup_unconfirmed', 'socket_unavailable', 'execution_failed'].includes(code) ? 503 : - ['invalid_model_output', 'tool_choice_not_met'].includes(code) ? 422 : code === 'request_bound' ? 413 : 400; + ['invalid_model_output', 'tool_choice_not_met', 'execution_budget_exceeded'].includes(code) ? 422 : + code === 'request_bound' ? 413 : 400; errorReply(response, status, code); } finally { client.close(); diff --git a/src/opencode-bridge.cjs b/src/opencode-bridge.cjs index 1a743c4..7476012 100644 --- a/src/opencode-bridge.cjs +++ b/src/opencode-bridge.cjs @@ -17,7 +17,7 @@ const FAILURE_CODES = new Set([ 'event_disposed', 'event_response', 'event_bound', 'event_invalid', 'event_closed', 'event_transport', 'session', 'model', 'prompt'].map(code => `opencode_${code}`), ]); -const PROVIDER_ERRORS = Object.freeze(['execution_failed', 'invalid_model_output', 'tool_choice_not_met', +const PROVIDER_ERRORS = Object.freeze(['execution_failed', 'execution_budget_exceeded', 'invalid_model_output', 'tool_choice_not_met', 'invalid_conversation', 'request_bound', 'busy', 'cancelled', 'cleanup_unconfirmed', 'socket_unavailable', 'socket_error', 'disconnected', 'invalid_response', 'incompatible_capabilities', 'provider_failed', 'other']); @@ -63,6 +63,11 @@ function validTaskDiagnostic(value) { } function validProviderDiagnostic(value) { const template = emptyProviderDiagnostic(); + // Historical v1 receipts predate the negotiated budget code. Accept either + // exact closed schema; never fabricate the missing counter or relax extras. + if (record(value?.request_errors) && !Object.hasOwn(value.request_errors, 'execution_budget_exceeded')) { + delete template.request_errors.execution_budget_exceeded; + } const matches = (actual, expected) => record(actual) && Object.keys(actual).length === Object.keys(expected).length && Object.entries(expected).every(([key, item]) => Object.hasOwn(actual, key) && (record(item) ? matches(actual[key], item) : typeof item === 'number' diff --git a/src/private-compute.cjs b/src/private-compute.cjs index 09f0d7d..06a9983 100644 --- a/src/private-compute.cjs +++ b/src/private-compute.cjs @@ -275,13 +275,18 @@ class PrivateCompute { } } + // Only the negotiated conversation subclass can recognize this additive code. + // Q&A and legacy conversations retain their original closed error vocabulary. + _allowsExecutionBudgetError(_message) { return false; } + _response(message) { requireValue(object(message) && message.version === 1 && typeof message.event === 'string'); requireValue((typeof message.id === 'string' && /^[0-9a-f]{32}$/.test(message.id)) || (message.id === null && message.event === 'error' && message.code === 'invalid_request')); if (message.event === 'error') { keys(message, ['version', 'id', 'event', 'code']); - requireValue(REMOTE_ERRORS.has(message.code)); + const budgetError = message.code === 'execution_budget_exceeded' && this._allowsExecutionBudgetError(message); + requireValue(REMOTE_ERRORS.has(message.code) || budgetError); const error = failure(message.code); if (message.id === null) throw error; requireValue(message.id === this.handshake?.id || message.id === this.pending?.id || @@ -298,7 +303,7 @@ class PrivateCompute { // through cleanup. Uncertainty takes precedence as cleanup_unconfirmed. // Do not infer this receipt from an error code before admission or from a // transport failure that merely has the same message. - if (this.pending.admitted && ['execution_failed', 'cancelled'].includes(message.code)) { + if (this.pending.admitted && (['execution_failed', 'cancelled'].includes(message.code) || budgetError)) { cleanedFailures.add(error); } this._settle(error); diff --git a/src/private-conversation.cjs b/src/private-conversation.cjs index 9585c6b..79663d8 100644 --- a/src/private-conversation.cjs +++ b/src/private-conversation.cjs @@ -63,11 +63,12 @@ function equalLimits(value, expected, optional = []) { keys(value, Object.keys(expected), optional); check(Object.entries(expected).every(([key, item]) => value[key] === item), 'incompatible_capabilities'); } -function capabilities(value, generationPolicyVersion) { +function capabilities(value, generationPolicyVersion, executionErrorVersion) { check(object(value)); const generation = generationPolicyVersion === 1 ? ['generation_policy_version', 'generation_policies'] : []; + const execution = executionErrorVersion === 1 ? ['execution_error_version'] : []; equalLimits(value, { ...expectedLimits(value.model_profile), execution_slots: 1, - max_request_bytes: requestLimit(value.model_profile), max_response_bytes: 65536 }, ['max_seconds', 'quarantined', ...generation]); + max_request_bytes: requestLimit(value.model_profile), max_response_bytes: 65536 }, ['max_seconds', 'quarantined', ...generation, ...execution]); check(Number.isInteger(value.max_seconds) && value.max_seconds >= 1 && value.max_seconds <= 600 && typeof value.quarantined === 'boolean', 'incompatible_capabilities'); if (generationPolicyVersion === 1) { @@ -76,6 +77,7 @@ function capabilities(value, generationPolicyVersion) { JSON.stringify(value.generation_policies) === JSON.stringify(expected), 'unsupported_generation_policy'); Object.freeze(value.generation_policies); } + if (executionErrorVersion === 1) check(value.execution_error_version === 1, 'incompatible_capabilities'); return Object.freeze(value); } @@ -168,10 +170,12 @@ function validateResult(value, caps, input) { } class PrivateConversation extends PrivateCompute { - constructor(socketPath, { generationPolicyVersion } = {}) { + constructor(socketPath, { generationPolicyVersion, executionErrorVersion } = {}) { super(socketPath); check(generationPolicyVersion === undefined || generationPolicyVersion === 1, 'unsupported_generation_policy'); this.generationPolicyVersion = generationPolicyVersion; + check(executionErrorVersion === undefined || executionErrorVersion === 1, 'unsupported_execution_error_version'); + this.executionErrorVersion = executionErrorVersion; } // These hooks reuse only the already-tested transport. Q&A callers and bytes // remain unchanged; this instance cannot silently use the old handshake. @@ -179,7 +183,8 @@ class PrivateConversation extends PrivateCompute { // The separate conversation family may advertise a larger request envelope; // the legacy Q&A class and its 32KiB framing remain byte-for-byte unchanged. if (operation.type === 'capabilities') operation = { type: 'conversation_capabilities', - ...(this.generationPolicyVersion === 1 ? { generation_policy_version: 1 } : {}) }; + ...(this.generationPolicyVersion === 1 ? { generation_policy_version: 1 } : {}), + ...(this.executionErrorVersion === 1 ? { execution_error_version: 1 } : {}) }; try { const body = Buffer.from(JSON.stringify({ version: 1, id, operation })); check(body.length > 0 && body.length <= (this.caps?.max_request_bytes ?? 32768), 'request_bound'); @@ -203,18 +208,23 @@ class PrivateConversation extends PrivateCompute { return new Promise((resolve, reject) => { const abort = () => this._cancel(); this.pending = { id, input, resolve, reject, signal, abort, admitted: false, cancelled: false, + executionErrorVersion: this.caps.execution_error_version === 1 ? 1 : undefined, timer: setTimeout(() => this._cancel(), this.caps.max_seconds * 1000 + 5000) }; signal?.addEventListener('abort', abort, { once: true }); this._send(id, { type: 'submit_conversation', conversation: input }); if (signal?.aborted) abort(); }); } + _allowsExecutionBudgetError(message) { + return this.state === 'open' && this.pending?.admitted === true && + this.pending.executionErrorVersion === 1 && message.id === this.pending.id && !this.cancels.has(message.id); + } _response(message) { check(object(message) && message.version === 1 && message.event !== 'capabilities', 'invalid_response'); if (message.event === 'conversation_capabilities') { keys(message, ['version', 'id', 'event', 'capabilities']); check(this.state === 'connecting' && message.id === this.handshake?.id, 'invalid_response'); - this.caps = capabilities(message.capabilities, this.generationPolicyVersion); + this.caps = capabilities(message.capabilities, this.generationPolicyVersion, this.executionErrorVersion); this.state = 'open'; clearTimeout(this.handshake.timer); this.handshake.resolve(this.caps); diff --git a/tests/chat-completions-provider.test.cjs b/tests/chat-completions-provider.test.cjs index 33f4980..698ab6b 100644 --- a/tests/chat-completions-provider.test.cjs +++ b/tests/chat-completions-provider.test.cjs @@ -11,7 +11,8 @@ const { startChatCompletionsProvider, toConversation } = require('../src/chat-co const { caps: legacyCaps, result: legacyResult, reply, fixture } = require('./conversation-fixture.cjs'); // These are synthetic replies for the explicitly negotiated worker policy. -const caps = model => ({ ...legacyCaps(model), generation_policy_version: 1, generation_policies: ['greedy_v1'] }); +const caps = model => ({ ...legacyCaps(model), generation_policy_version: 1, generation_policies: ['greedy_v1'], + execution_error_version: 1 }); const result = (output, model) => ({ ...legacyResult(output, model), generation_policy: 'greedy_v1' }); const MODEL = 'qwen3-0.6b-v1'; @@ -101,7 +102,8 @@ test('SDK stream shape releases actual text and token usage only after terminal assert.equal(settled, false); assert.deepEqual(f.provider.observations, { submitted: 1, completed: 0, incomplete: 0, cleanup_confirmed: 0 }); assert.deepEqual(f.requests.map(row => row.operation.type), ['conversation_capabilities', 'submit_conversation']); - assert.deepEqual(f.requests[0].operation, { type: 'conversation_capabilities', generation_policy_version: 1 }); + assert.deepEqual(f.requests[0].operation, { type: 'conversation_capabilities', generation_policy_version: 1, + execution_error_version: 1 }); const input = f.requests[1].operation.conversation; assert.equal(input.generation_policy, 'greedy_v1'); assert.equal(input.visibility, 'private_local'); @@ -310,6 +312,39 @@ test('transient core failures keep their retryable status without claiming a com } }); +test('negotiated admitted budget exhaustion is cleanup-confirmed HTTP422, never a completed model result', async t => { + const f = await start(t, (socket, message) => { + reply(socket, message, 'admitted'); + reply(socket, message, 'error', {code: 'execution_budget_exceeded'}); + }, true); + try { + const response = await send(f.provider, request()); + assert.equal(response.status, 422); + assert.equal(JSON.parse(response.body).error.code, 'execution_budget_exceeded'); + assert.deepEqual(f.requests[0].operation, {type: 'conversation_capabilities', generation_policy_version: 1, + execution_error_version: 1}); + assert.equal(f.provider.diagnostics.summary.request_errors.execution_budget_exceeded, 1); + assert.deepEqual(f.provider.observations, {submitted: 1, completed: 0, incomplete: 0, cleanup_confirmed: 1}); + assert.deepEqual(f.provider.diagnostics.records, []); + } finally { await f.provider.close(); } +}); + +test('budget code before admission or under another correlation is not a cleanup receipt', async t => { + for (const mode of ['unadmitted', 'wrong-id']) { + const f = await start(t, (socket, message) => { + if (mode === 'wrong-id') reply(socket, message, 'admitted'); + reply(socket, mode === 'wrong-id' ? {...message, id: 'f'.repeat(32)} : message, + 'error', {code: 'execution_budget_exceeded'}); + }, true); + try { + const response = await send(f.provider, request()); + assert.notEqual(response.status, 422); + assert.equal(f.provider.observations.cleanup_confirmed, 0); + assert.equal(f.provider.diagnostics.summary.request_errors.execution_budget_exceeded, 0); + } finally { await f.provider.close(); } + } +}); + test('reaped execution failure followed by success accounts for both cleanups, not two model results', async t => { let attempts = 0; const f = await start(t, (socket, message) => { diff --git a/tests/opencode-runtime.test.cjs b/tests/opencode-runtime.test.cjs index 6c63660..1607dd4 100644 --- a/tests/opencode-runtime.test.cjs +++ b/tests/opencode-runtime.test.cjs @@ -6,9 +6,26 @@ const assert = require('node:assert/strict'); const {spawn} = require('node:child_process'); const {PassThrough, Writable} = require('node:stream'); const {configuration, ownedOpenCode} = require('../src/opencode-runtime.cjs'); -const {readFrames, writeFrame, emptyProviderDiagnostic, emptyTaskDiagnostic} = require('../src/opencode-bridge.cjs'); +const {readFrames, writeFrame, emptyProviderDiagnostic, emptyTaskDiagnostic, + validProviderDiagnostic} = require('../src/opencode-bridge.cjs'); const READY = {type: 'ready', version: 1, execution: 'private_local', confidentialRemoteAvailable: false}; const RESULT = {text: 'Synthetic answer.', commands: 1, nativeTurnCompleted: true, taskVerified: false}; +test('provider counter schema accepts exact historical receipts and the new bounded terminal code', () => { + const current = emptyProviderDiagnostic(); + current.request_errors.execution_budget_exceeded = 1; + assert.equal(validProviderDiagnostic(current), true); + const old = structuredClone(current); + delete old.request_errors.execution_budget_exceeded; + assert.equal(validProviderDiagnostic(old), true); + assert.equal(Object.hasOwn(old.request_errors, 'execution_budget_exceeded'), false); + for (const changed of [true, -1, 65536, 'PRIVATE_CANARY']) { + const invalid = structuredClone(current); + invalid.request_errors.execution_budget_exceeded = changed; + assert.equal(validProviderDiagnostic(invalid), false); + } + old.request_errors.private_detail = 'PRIVATE_CANARY'; + assert.equal(validProviderDiagnostic(old), false); +}); function child(t, program) { const process = spawn(require('node:process').execPath, ['-e', program], {stdio: ['pipe', 'pipe', 'pipe'], env: {PATH: '/usr/bin:/bin', LANG: 'C.UTF-8', ELECTRON_RUN_AS_NODE: '1'}}); diff --git a/tests/opencode-session.test.cjs b/tests/opencode-session.test.cjs index c7bf932..f0fb074 100644 --- a/tests/opencode-session.test.cjs +++ b/tests/opencode-session.test.cjs @@ -19,7 +19,8 @@ function fixture(Task, receive, options = {}) { observations: options.observations ?? {submitted: 0, cleanup_confirmed: 0}, async close() { observed.push('provider-close'); if (options.badCleanup) throw Error('private detail'); }}; const hooks = {Task, preflight: async () => options.capabilities ?? - {...caps(options.model ?? DEFAULT_MODEL), generation_policy_version: 1, generation_policies: ['greedy_v1']}, + {...caps(options.model ?? DEFAULT_MODEL), generation_policy_version: 1, generation_policies: ['greedy_v1'], + execution_error_version: 1}, provider: async value => { binding.provider = value.model; return provider; }, prepare(cooperative) { assert.equal(cooperative, options.cooperative ?? false); }, cooperative: () => options.cooperative ?? false, port: async () => 1235, approvalMs: 15, @@ -85,10 +86,11 @@ test('owner-selected validated 4B core binds provider, native catalog and task b test('incompatible, widened or quarantined core fails before provider or native startup', async () => { const model = 'qwen3-4b-instruct-2507-v1'; for (const changed of [{quarantined: true}, {local_only: false}, {max_prompt_tokens: 262144}, - {generation_policies: []}, {model_profile: 'unreviewed-model'}]) { + {generation_policies: []}, {execution_error_version: undefined}, {execution_error_version: 2}, + {model_profile: 'unreviewed-model'}]) { class Task { constructor() { assert.fail('must not create task'); } } const f = fixture(Task, () => assert.fail('must not signal ready'), {capabilities: { - ...caps(model), generation_policy_version: 1, generation_policies: ['greedy_v1'], ...changed, + ...caps(model), generation_policy_version: 1, generation_policies: ['greedy_v1'], execution_error_version: 1, ...changed, }}); assert.equal(await f.done, 1); assert.deepEqual(f.binding, {}); assert.deepEqual(f.observed, []); @@ -124,7 +126,7 @@ test('owner cleanup accepts an actual provider retry after a correlated reaped f reply(socket, request, 'admitted'); if (++attempts === 1) reply(socket, request, 'error', {code: 'execution_failed'}); else reply(socket, request, 'result', {result: coreResult(undefined, model)}); - }, {...caps(model), generation_policy_version: 1, generation_policies: ['greedy_v1']}); + }, {...caps(model), generation_policy_version: 1, generation_policies: ['greedy_v1'], execution_error_version: 1}); const provider = await startChatCompletionsProvider({socketPath: core.socketPath, model, diagnostics: true}); class Task { async run() { diff --git a/tests/private-compute.test.cjs b/tests/private-compute.test.cjs index 85a7624..4a942b2 100644 --- a/tests/private-compute.test.cjs +++ b/tests/private-compute.test.cjs @@ -8,7 +8,7 @@ const net = require('node:net'); const os = require('node:os'); const path = require('node:path'); const { test } = require('node:test'); -const { PrivateCompute } = require('../src/private-compute.cjs'); +const { PrivateCompute, terminalCleanupConfirmed } = require('../src/private-compute.cjs'); function caps(overrides = {}) { return { visibility: 'private_local', local_only: true, model_profile: 'smollm2-360m-v1', @@ -78,6 +78,20 @@ async function fixture(t, handler, options = {}) { return { client, socketPath, directory, requests, sockets }; } +test('legacy Q&A never accepts the conversational terminal budget vocabulary', async t => { + const f = await fixture(t, (socket, request) => { + reply(socket, request, 'admitted'); + reply(socket, request, 'error', {code: 'execution_budget_exceeded'}); + }); + await f.client.connect(); + await assert.rejects(f.client.ask({question: 'Q', context: 'C'}), error => { + assert.equal(error.code, 'invalid_response'); + assert.equal(terminalCleanupConfirmed(error), false); + return true; + }); + assert.deepEqual(f.requests[0].operation, {type: 'capabilities'}); +}); + test('real Unix framing preserves the exact private result across partial and coalesced frames', async t => { const original = answer(); const f = await fixture(t, (socket, request) => { diff --git a/tests/private-conversation.test.cjs b/tests/private-conversation.test.cjs index 6241677..5a8b1fa 100644 --- a/tests/private-conversation.test.cjs +++ b/tests/private-conversation.test.cjs @@ -23,6 +23,80 @@ test('actual framed socket uses separate handshake and yields exact cleanup-conf await assert.rejects(f.client.ask({ question: 'legacy', context: 'must not send' })); }); +test('execution error negotiation is independent, strict, and unchanged when absent', async t => { + for (const value of [null, 0, 2, true]) { + assert.throws(() => new PrivateConversation('/fixture/core.sock', {executionErrorVersion: value}), + /unsupported_execution_error_version/); + } + for (const advertised of [caps(), {...caps(), execution_error_version: null}, + {...caps(), execution_error_version: 2}]) { + const f = await fixture(t, () => assert.fail('must not submit'), advertised, {executionErrorVersion: 1}); + await assert.rejects(f.client.connect(), {code: 'incompatible_capabilities'}); + assert.deepEqual(f.requests[0].operation, {type: 'conversation_capabilities', execution_error_version: 1}); + } + const unsolicited = await fixture(t, () => assert.fail('must not submit'), {...caps(), execution_error_version: 1}); + await assert.rejects(unsolicited.client.connect()); +}); + +test('only a negotiated admitted task has a budget-error cleanup receipt, with request-local provenance', async t => { + let submitted = 0; + const f = await fixture(t, (socket, request) => { + if (++submitted === 1) reply(socket, request, 'admitted'); + reply(socket, request, 'error', {code: 'execution_budget_exceeded'}); + }, {...caps(), execution_error_version: 1}, {executionErrorVersion: 1}); + await f.client.connect(); + let accepted; + await assert.rejects(f.client.submit(input()), error => { + accepted = error; + assert.equal(error.code, 'execution_budget_exceeded'); + assert.equal(terminalCleanupConfirmed(error), true); + return true; + }); + await assert.rejects(f.client.submit(input()), error => { + assert.equal(error.code, 'invalid_response'); + assert.equal(terminalCleanupConfirmed(error), false); + return true; + }); + assert.equal(terminalCleanupConfirmed({...accepted, cleanupConfirmed: true}), false); + assert.equal(terminalCleanupConfirmed(Object.assign(Error(accepted.message), {code: accepted.code})), false); + assert.equal(terminalCleanupConfirmed(accepted), true); +}); + +test('legacy, wrong-id, cancel-id and handshake budget codes never become cleanup receipts', async t => { + for (const mode of ['legacy', 'wrong-id', 'cancel-id']) { + let admitted; + const ready = new Promise(resolve => { admitted = resolve; }); + const f = await fixture(t, (socket, request) => { + if (request.operation.type === 'submit_conversation') { + reply(socket, request, 'admitted'); + if (mode === 'cancel-id') { admitted(); return; } + } + reply(socket, mode === 'wrong-id' ? {...request, id: 'f'.repeat(32)} : request, + 'error', {code: 'execution_budget_exceeded'}); + }, mode === 'legacy' ? caps() : {...caps(), execution_error_version: 1}, + mode === 'legacy' ? {} : {executionErrorVersion: 1}); + await f.client.connect(); + const controller = new AbortController(); + const pending = f.client.submit(input(), {signal: controller.signal}); + const rejected = assert.rejects(pending, error => { + assert.equal(error.code, 'invalid_response', mode); + assert.equal(terminalCleanupConfirmed(error), false, mode); + return true; + }); + if (mode === 'cancel-id') { await ready; controller.abort(); } + await rejected; + } + // Pure dispatch guard at the handshake boundary; no synthetic execution claim. + const client = new PrivateConversation('/fixture/core.sock', {executionErrorVersion: 1}); + client.state = 'connecting'; client.handshake = {id: 'a'.repeat(32)}; + assert.throws(() => client._response({version: 1, id: client.handshake.id, event: 'error', + code: 'execution_budget_exceeded'}), error => { + assert.equal(error.code, 'invalid_response'); + assert.equal(terminalCleanupConfirmed(error), false); + return true; + }); +}); + test('negotiated greedy requests require matching result evidence and cannot silently use a legacy core', async t => { const model = 'qwen3-0.6b-v1'; const negotiated = { ...caps(model), generation_policy_version: 1, generation_policies: ['greedy_v1'] }; diff --git a/tests/real_opencode_provider_errors.cjs b/tests/real_opencode_provider_errors.cjs index 421eeee..1d97192 100644 --- a/tests/real_opencode_provider_errors.cjs +++ b/tests/real_opencode_provider_errors.cjs @@ -31,15 +31,19 @@ async function terminalCase(reason, config) { const coding = message.operation.conversation.tools.some(tool => tool.name === 'bash'); if (coding) assert.equal(++codingSubmissions, 1, 'terminal output must not cause native regeneration'); // Titles and other upstream no-tool requests are not the coding task. - const output = coding ? {type: 'incomplete', reason} : {type: 'assistant', text: 'Synthetic retry check'}; reply(socket, message, 'admitted'); - reply(socket, message, 'result', {result: result(output, MODEL)}); + if (coding && reason === 'execution_budget_exceeded') { + reply(socket, message, 'error', {code: reason}); + } else { + const output = coding ? {type: 'incomplete', reason} : {type: 'assistant', text: 'Synthetic retry check'}; + reply(socket, message, 'result', {result: {...result(output, MODEL), generation_policy: 'greedy_v1'}}); + } } catch (error) { fixtureError ??= error; socket.destroy(); abort.abort(); } - }, caps(MODEL)); + }, {...caps(MODEL), generation_policy_version: 1, generation_policies: ['greedy_v1'], execution_error_version: 1}); phase = 'native_start'; runtime = await OpenCodeRuntime.start({...config, socketPath: core.socketPath}, {workspace: project}); phase = 'native_task'; @@ -59,11 +63,13 @@ async function terminalCase(reason, config) { assert.equal(codingSubmissions, 1); assert.equal(approvals, 0); const diagnostics = runtime.diagnostics; - assert.equal(diagnostics.incomplete_reasons[reason], 1); - assert.equal(diagnostics.request_errors.invalid_model_output, 1); + const budget = reason === 'execution_budget_exceeded'; + if (!budget) assert.equal(diagnostics.incomplete_reasons[reason], 1); + assert.equal(diagnostics.request_errors.invalid_model_output, budget ? 0 : 1); + assert.equal(diagnostics.request_errors.execution_budget_exceeded, budget ? 1 : 0); assert.equal(diagnostics.submitted, submissions); assert.equal(diagnostics.cleanup_confirmed, submissions); - assert.equal(diagnostics.incomplete, 1); + assert.equal(diagnostics.incomplete, budget ? 0 : 1); assert.equal(await fs.readFile(path.join(project, 'README.txt'), 'utf8'), marker); assert.deepEqual(await fs.readdir(project), ['README.txt']); phase = 'session_close'; @@ -113,7 +119,7 @@ async function main(args = process.argv.slice(2)) { const config = {version: 1, opencode: report.binary, opencodeSha256: report.binary_sha256, buildReport, node, nodeSha256: hash(await fs.readFile(node))}; const cases = []; - for (const reason of ['invalid_output', 'wire_truncated']) cases.push(await terminalCase(reason, config)); + for (const reason of ['invalid_output', 'wire_truncated', 'execution_budget_exceeded']) cases.push(await terminalCase(reason, config)); const evidence = {version: 1, native_runtime: true, runtime_version: report.runtime_version, source_commit: report.source_commit, binary_sha256: report.binary_sha256, node_sha256: config.nodeSha256, provider_sha256: hash(await fs.readFile(path.join(__dirname, '../src/chat-completions-provider.cjs'))), diff --git a/tests/test_smoke_opencode_inference.py b/tests/test_smoke_opencode_inference.py index e31cacc..75da375 100644 --- a/tests/test_smoke_opencode_inference.py +++ b/tests/test_smoke_opencode_inference.py @@ -47,7 +47,7 @@ def test_exact_synthetic_inventory_is_bound_without_clean_git_claim(self): def test_profiles_are_closed_and_default_resources_are_unchanged(self): self.assertEqual(TRIAL.trial_profile(), dict(model_profile='qwen3-0.6b-v1', - core_revision='845cc84d0d0b766ab1c5227231dbf6c8eaeb8cc3', guest_memory_mib=6144, + core_revision='08f23e11ce883f546262ec424940400283d58f22', guest_memory_mib=6144, core_memory_bytes=5 * TRIAL.GIB, qemu_memory_bytes=7 * TRIAL.GIB, host_available_bytes=8 * TRIAL.GIB, provision_budget_bytes=5 * TRIAL.GIB, scratch_gib=18, memory_failure='host_available_memory_below_8GiB')) @@ -57,7 +57,10 @@ def test_profiles_are_closed_and_default_resources_are_unchanged(self): def test_larger_profile_is_separate_and_requires_a_real_pin(self): self.assertRegex(TRIAL.LARGE_CORE, r'^[0-9a-f]{40}$') - self.assertNotEqual(TRIAL.LARGE_CORE, TRIAL.CORE) + # One reviewed core supports both profiles and the new negotiated error + # contract. Separate models/resources do not require artificial source forks. + self.assertEqual(TRIAL.LARGE_CORE, '08f23e11ce883f546262ec424940400283d58f22') + self.assertNotEqual(TRIAL.LARGE_MODEL, TRIAL.MODEL) with patch.object(TRIAL, 'LARGE_CORE', None): with self.assertRaisesRegex(ValueError, 'larger_core_not_pinned'): TRIAL.trial_profile(TRIAL.LARGE_MODEL) From 63f73faa2c485f3f2a736d838bfc5fd14817fc37 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:46:26 +0200 Subject: [PATCH 02/39] fix(opencode): forward configured generation policy and prove terminal stops --- docs/OPENCODE.md | 44 +++++++++++++++----- scripts/smoke_opencode_inference.py | 4 +- src/opencode-config.cjs | 5 ++- tests/opencode-config.test.cjs | 23 ++++++++-- tests/real-opencode-provider-errors.test.cjs | 36 ++++++++++++++++ tests/real_opencode_provider_errors.cjs | 24 +++++++++-- tests/test_smoke_opencode_inference.py | 4 +- 7 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 tests/real-opencode-provider-errors.test.cjs diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index e652534..b077505 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -77,7 +77,8 @@ Before starting OpenCode, the owner validates the complete core capability reply including template, limits, private-local scope and negotiated `greedy_v1` policy. Current native sessions also require `execution_error_version: 1`; an older core without that capability is refused before starting the runtime. Both native smoke -profiles pin core `08f23e11ce883f546262ec424940400283d58f22` for this contract. +profiles pin core `a5246942707890978867930dca518a29d1d48bd5` for this contract, +including the preserved main integrations and reviewed source-quality fixes. Legacy Q&A and conversation clients that do not opt in remain unchanged. That one model identity binds the native catalog, all agent roles, provider, @@ -440,9 +441,31 @@ missing/conflicting result evidence is an error, not a sampled fallback. An older ordinary conversation client can still use its unchanged handshake; omitting the policy retains the core's previous profile behavior. -This corrects a real contract mismatch: the earlier adapter accepted zero -temperature while the Qwen worker used its default sampled 0.7/0.8/20 profile. -The fixed worker passes `do_sample:false,num_beams:1`; model, token budgets, +An actual-runtime regression exposed an additional configuration mismatch: +pinned OpenCode omits temperature unless the custom model advertises that +capability. Setting only `agent.temperature:0` did not select greedy execution. +Both supported profiles now explicitly set model `temperature:true`, retaining +the existing zero setting on every coding role. Prompts, model identities, +permissions and resource budgets are unchanged; this corrects which generation +policy actually reaches the core, rather than proving better model quality. + +The pinned native runtime has now been exercised with the production launcher +and provider but **synthetic** core/model replies. Each of `invalid_output`, +`wire_truncated` and `execution_budget_exceeded` made exactly one coding request, +with observed `greedy_v1`, no native retry, no approval and confirmed session +cleanup; the disposable project remained unchanged. The terminal budget error +was not counted as a completed or incomplete model result. The synthetic result +fixture mirrors only the policy actually present on the request; negotiated +support alone is not a selected policy. This proves runtime policy forwarding +and terminal-error behavior, not actual inference or private peer execution. +Local report `native-errors-budget-greedy-df481-20261004.json` was captured on +the explicitly modified `df481f3f` candidate; SHA-256: +`a08f26ab2c61b2653949dadf06f45b9402066383ee651d573a5b14f9623e3786`. +Earlier real-model failures retain their original sources and outcomes. + +An earlier adapter correction addressed a separate contract mismatch: it accepted +zero temperature while the Qwen worker used its sampled 0.7/0.8/20 profile. +The worker's greedy path passes `do_sample:false,num_beams:1`; model, token budgets, tool permissions, task and independent success checks remain unchanged. Protocol/backend-double checks verify the wiring, not model quality. The earlier [run `37066003771`](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37066003771) @@ -455,12 +478,13 @@ stop, and greedy generation does not guarantee a completed coding task. `scripts/smoke_opencode_inference.py` prepares one explicit disposable Debian 13 KVM trial; it does not install or run the model on the development host. Its `pack` mode captures each Code source/runtime file by hash and the exact core -archive selected by its explicit model profile. The default Qwen3-0.6B profile -retains core `845cc84d0d0b766ab1c5227231dbf6c8eaeb8cc3`; the separate -`qwen3-4b-instruct-2507-v1` candidate binds core -`1297f8f1a5d163d802efd066c51a950b95588fa5`. This includes the bounded -provisioning-timeout recovery from core `3aa0e2d0` and existing closed worker -diagnostics. The previous `37209881216` attempt remains failed before model +archive selected by its explicit model profile. Both current profiles pin core +`a5246942707890978867930dca518a29d1d48bd5`, while retaining their different +models and resource profiles. Historical 0.6B trials used `845cc84d`; the last +4B trial used `1297f8f1a5d163d802efd066c51a950b95588fa5`, including the bounded +provisioning-timeout recovery from `3aa0e2d0`. Neither those historical outcomes +nor the synthetic native-runtime probe proves the current real-model candidate. +The previous `37209881216` attempt remains failed before model execution. [Trial `37217032474`](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37217032474) on the previous core `f25352df` completed the pinned 4B provisioning and returned one actual model result, but the native task failed at `invalid_output` after 152,995 ms. No tools, edits diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 440e7d5..e841ff8 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -27,12 +27,12 @@ import uuid ROOT = Path(__file__).resolve().parents[1] -CORE = '08f23e11ce883f546262ec424940400283d58f22' +CORE = 'a5246942707890978867930dca518a29d1d48bd5' MODEL = 'qwen3-0.6b-v1' GIB = 1024 ** 3 LARGE_MODEL = 'qwen3-4b-instruct-2507-v1' # Both profiles require the reviewed terminal-budget IPC; model and budgets stay distinct. -LARGE_CORE = '08f23e11ce883f546262ec424940400283d58f22' +LARGE_CORE = 'a5246942707890978867930dca518a29d1d48bd5' MODEL_PROFILES = (MODEL, LARGE_MODEL) ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/src/opencode-config.cjs b/src/opencode-config.cjs index fc70607..2c2b484 100644 --- a/src/opencode-config.cjs +++ b/src/opencode-config.cjs @@ -51,7 +51,10 @@ function runtimeSettings({baseUrl, bearerToken, password, cooperative = false, m models: {[model]: {name: 'VOLPAROSSA core conversation', limit: {context: expectedLimits(model).model_context_tokens, output: expectedLimits(model).max_new_tokens}, tool_call: true, reasoning: false, - modalities: {input: ['text'], output: ['text']}}}, + modalities: {input: ['text'], output: ['text']}, + // Pinned OpenCode otherwise drops agent.temperature, silently omitting + // the zero-temperature request that selects core's negotiated greedy_v1. + temperature: true}}, }}, }; const env = { diff --git a/tests/opencode-config.test.cjs b/tests/opencode-config.test.cjs index b0099f2..e3bb04c 100644 --- a/tests/opencode-config.test.cjs +++ b/tests/opencode-config.test.cjs @@ -27,13 +27,30 @@ test('pinned runtime uses only core provider with separate one-shot tool boundar assert.equal(Object.hasOwn(env, 'HOME'), false); assert.equal(Object.hasOwn(env, 'OPENAI_API_KEY'), false); }); -test('legacy 0.6B defaults, prompts and permissions remain byte-identical', () => { +test('legacy 0.6B settings change only the explicit temperature capability', () => { const golden = JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures/opencode-default-settings.json'), 'utf8')); for (const cooperative of [false, true]) { + const expected = structuredClone(golden[cooperative ? 'cooperative' : 'default']); + assert.equal(Object.hasOwn(expected.config.provider.volparossa.models[MODEL], 'temperature'), false); + expected.config.provider.volparossa.models[MODEL].temperature = true; + expected.env.OPENCODE_CONFIG_CONTENT = JSON.stringify(expected.config); // Direct exact bytes, including key order and the synthetic environment; - // this regression fixture is not a password-storage or authentication hash. + // the historical fixture stays unchanged; no prompt, limit, permission or + // other environment change can hide behind this explicit capability delta. assert.equal(JSON.stringify(runtimeSettings({...input, cooperative})), - JSON.stringify(golden[cooperative ? 'cooperative' : 'default'])); + JSON.stringify(expected)); + } +}); +test('both model profiles advertise temperature so every coding role actually requests zero', () => { + for (const model of [MODEL, 'qwen3-4b-instruct-2507-v1']) { + for (const cooperative of [false, true]) { + const {config, env} = runtimeSettings({...input, model, cooperative}); + assert.equal(config.provider.volparossa.models[model].temperature, true); + for (const role of ['build', 'general', 'explore']) { + assert.equal(config.agent[role].temperature, 0); + } + assert.deepEqual(JSON.parse(env.OPENCODE_CONFIG_CONTENT), config); + } } }); test('4B settings use only the exact core-selected profile without changing task or tool policy', () => { diff --git a/tests/real-opencode-provider-errors.test.cjs b/tests/real-opencode-provider-errors.test.cjs new file mode 100644 index 0000000..0459950 --- /dev/null +++ b/tests/real-opencode-provider-errors.test.cjs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Synthetic frame bindings only; this test never starts OpenCode or a model. +'use strict'; +const assert = require('node:assert/strict'); +const {test} = require('node:test'); +const {syntheticResult} = require('./real_opencode_provider_errors.cjs'); +const {caps, input} = require('./conversation-fixture.cjs'); +const {validateResult} = require('../src/private-conversation.cjs'); +const MODEL = 'qwen3-0.6b-v1'; +const negotiated = {...caps(MODEL), generation_policy_version: 1, generation_policies: ['greedy_v1'], + execution_error_version: 1}; + +test('native error fixture does not invent policy from negotiated support', () => { + const conversation = input(); + for (const reason of ['invalid_output', 'wire_truncated']) { + const response = syntheticResult({type: 'incomplete', reason}, conversation); + assert.equal(Object.hasOwn(response, 'generation_policy'), false); + assert.equal(validateResult(response, negotiated, conversation), response); + assert.throws(() => validateResult({...response, generation_policy: 'greedy_v1'}, negotiated, conversation), + {code: 'invalid_conversation'}); + } +}); + +test('native error fixture retains the exact explicitly selected greedy policy', () => { + const conversation = {...input(), generation_policy: 'greedy_v1'}; + for (const reason of ['invalid_output', 'wire_truncated']) { + const response = syntheticResult({type: 'incomplete', reason}, conversation); + assert.equal(response.generation_policy, 'greedy_v1'); + assert.equal(validateResult(response, negotiated, conversation), response); + const {generation_policy: removed, ...missing} = response; + assert.equal(removed, 'greedy_v1'); + assert.throws(() => validateResult(missing, negotiated, conversation), {code: 'generation_policy_mismatch'}); + } + assert.throws(() => syntheticResult({type: 'assistant', text: 'synthetic'}, + {...conversation, generation_policy: 'unknown'})); +}); diff --git a/tests/real_opencode_provider_errors.cjs b/tests/real_opencode_provider_errors.cjs index 1d97192..25c1df7 100644 --- a/tests/real_opencode_provider_errors.cjs +++ b/tests/real_opencode_provider_errors.cjs @@ -12,6 +12,14 @@ const {caps, result, reply, fixture} = require('./conversation-fixture.cjs'); const MODEL = 'qwen3-0.6b-v1'; const hash = value => createHash('sha256').update(value).digest('hex'); +function syntheticResult(output, conversation) { + // Negotiating support does not select a policy. Mirror only the policy on + // the actual request, just as the core's checked result binding requires. + const selected = Object.hasOwn(conversation, 'generation_policy'); + if (selected) assert.equal(conversation.generation_policy, 'greedy_v1'); + return {...result(output, MODEL), ...(selected ? {generation_policy: conversation.generation_policy} : {})}; +} + async function terminalCase(reason, config) { const project = await fs.mkdtemp(path.join(os.tmpdir(), 'volparossa-opencode-error-')); await fs.chmod(project, 0o700); @@ -21,22 +29,30 @@ async function terminalCase(reason, config) { const abort = new AbortController(); const timer = setTimeout(() => abort.abort(), 90000); let runtime, fixtureError, taskError, submissions = 0, codingSubmissions = 0, approvals = 0; + const generationPolicies = {unspecified: 0, greedy_v1: 0}; let phase = 'fixture'; try { const core = await fixture({after: action => cleanups.push(action)}, (socket, message) => { try { assert.equal(message.operation.type, 'submit_conversation'); assert.equal(message.operation.conversation.visibility, 'private_local'); + const conversation = message.operation.conversation; + const selectedPolicy = Object.hasOwn(conversation, 'generation_policy'); + if (selectedPolicy) assert.equal(conversation.generation_policy, 'greedy_v1'); + generationPolicies[selectedPolicy ? 'greedy_v1' : 'unspecified']++; assert.ok(++submissions <= 8, 'bounded fixture requests'); const coding = message.operation.conversation.tools.some(tool => tool.name === 'bash'); - if (coding) assert.equal(++codingSubmissions, 1, 'terminal output must not cause native regeneration'); + if (coding) { + assert.equal(conversation.generation_policy, 'greedy_v1', 'native coding must select its configured policy'); + assert.equal(++codingSubmissions, 1, 'terminal output must not cause native regeneration'); + } // Titles and other upstream no-tool requests are not the coding task. reply(socket, message, 'admitted'); if (coding && reason === 'execution_budget_exceeded') { reply(socket, message, 'error', {code: reason}); } else { const output = coding ? {type: 'incomplete', reason} : {type: 'assistant', text: 'Synthetic retry check'}; - reply(socket, message, 'result', {result: {...result(output, MODEL), generation_policy: 'greedy_v1'}}); + reply(socket, message, 'result', {result: syntheticResult(output, conversation)}); } } catch (error) { fixtureError ??= error; @@ -76,7 +92,7 @@ async function terminalCase(reason, config) { await runtime.close(); runtime = null; return {reason, coding_submissions: codingSubmissions, auxiliary_submissions: submissions - codingSubmissions, - native_retry_count: 0, terminal_error: taskError.code, approvals, + native_retry_count: 0, terminal_error: taskError.code, approvals, generation_policies: generationPolicies, original_project_unchanged: true, session_cleanup_confirmed: true, provider_diagnostics: diagnostics}; } catch (error) { process.stderr.write(JSON.stringify({reason, phase, coding_submissions: codingSubmissions, @@ -134,4 +150,4 @@ if (require.main === module) main().catch(error => { process.stderr.write(`Native retry check failed: ${String(error.message).slice(0, 200)}\n`); process.exitCode = 1; }); -module.exports = {main}; +module.exports = {main, syntheticResult}; diff --git a/tests/test_smoke_opencode_inference.py b/tests/test_smoke_opencode_inference.py index 75da375..4d12007 100644 --- a/tests/test_smoke_opencode_inference.py +++ b/tests/test_smoke_opencode_inference.py @@ -47,7 +47,7 @@ def test_exact_synthetic_inventory_is_bound_without_clean_git_claim(self): def test_profiles_are_closed_and_default_resources_are_unchanged(self): self.assertEqual(TRIAL.trial_profile(), dict(model_profile='qwen3-0.6b-v1', - core_revision='08f23e11ce883f546262ec424940400283d58f22', guest_memory_mib=6144, + core_revision='a5246942707890978867930dca518a29d1d48bd5', guest_memory_mib=6144, core_memory_bytes=5 * TRIAL.GIB, qemu_memory_bytes=7 * TRIAL.GIB, host_available_bytes=8 * TRIAL.GIB, provision_budget_bytes=5 * TRIAL.GIB, scratch_gib=18, memory_failure='host_available_memory_below_8GiB')) @@ -59,7 +59,7 @@ def test_larger_profile_is_separate_and_requires_a_real_pin(self): self.assertRegex(TRIAL.LARGE_CORE, r'^[0-9a-f]{40}$') # One reviewed core supports both profiles and the new negotiated error # contract. Separate models/resources do not require artificial source forks. - self.assertEqual(TRIAL.LARGE_CORE, '08f23e11ce883f546262ec424940400283d58f22') + self.assertEqual(TRIAL.LARGE_CORE, 'a5246942707890978867930dca518a29d1d48bd5') self.assertNotEqual(TRIAL.LARGE_MODEL, TRIAL.MODEL) with patch.object(TRIAL, 'LARGE_CORE', None): with self.assertRaisesRegex(ValueError, 'larger_core_not_pinned'): From 714904a3ec452b8e10d8b40be6ac85107606ca3b Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:09:08 +0200 Subject: [PATCH 03/39] Pin native trials to reviewed generation diagnostic source fix --- docs/OPENCODE.md | 15 +++++++++++++-- scripts/smoke_opencode_inference.py | 4 ++-- tests/test_smoke_opencode_inference.py | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 35f7fa9..95b7547 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -77,7 +77,7 @@ Before starting OpenCode, the owner validates the complete core capability reply including template, limits, private-local scope and negotiated `greedy_v1` policy. Current native sessions also require `execution_error_version: 1`; an older core without that capability is refused before starting the runtime. Both native smoke -profiles pin core `a5246942707890978867930dca518a29d1d48bd5` for this contract, +profiles pin core `6a517b576baa17e7329661ee0476d1848081d114` for this contract, including the preserved main integrations and reviewed source-quality fixes. Legacy Q&A and conversation clients that do not opt in remain unchanged. @@ -506,11 +506,22 @@ stop, and greedy generation does not guarantee a completed coding task. KVM trial; it does not install or run the model on the development host. Its `pack` mode captures each Code source/runtime file by hash and the exact core archive selected by its explicit model profile. Both current profiles pin core -`a5246942707890978867930dca518a29d1d48bd5`, while retaining their different +`6a517b576baa17e7329661ee0476d1848081d114`, while retaining their different models and resource profiles. Historical 0.6B trials used `845cc84d`; the last 4B trial used `1297f8f1a5d163d802efd066c51a950b95588fa5`, including the bounded provisioning-timeout recovery from `3aa0e2d0`. Neither those historical outcomes nor the synthetic native-runtime probe proves the current real-model candidate. + +The earlier `a5246942` candidate's +[Quality run `37225595367`](https://github.com/VOLPAROSSA/volparossa/actions/runs/37225595367) +remains **failed**: strict Clippy rejected two 103-line diagnostic functions. +No native model trial was dispatched for that pair. The follow-up extracts the +existing snapshot parser and moves byte-identical Python test assertions into a +constant, without changing validation, model behavior, resources or acceptance +criteria. Its source checks are pending; this is not a rerun or a passing +real-model result. Original job `111504476222` log SHA-256: +`1ef3aca56b4052b9b56e2f9a469543e07cfdb1718d5a8706750c286f40f6d41c`. + The previous `37209881216` attempt remains failed before model execution. [Trial `37217032474`](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37217032474) on the previous core `f25352df` completed the pinned 4B provisioning and returned one actual model result, diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index e841ff8..e4adbfc 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -27,12 +27,12 @@ import uuid ROOT = Path(__file__).resolve().parents[1] -CORE = 'a5246942707890978867930dca518a29d1d48bd5' +CORE = '6a517b576baa17e7329661ee0476d1848081d114' MODEL = 'qwen3-0.6b-v1' GIB = 1024 ** 3 LARGE_MODEL = 'qwen3-4b-instruct-2507-v1' # Both profiles require the reviewed terminal-budget IPC; model and budgets stay distinct. -LARGE_CORE = 'a5246942707890978867930dca518a29d1d48bd5' +LARGE_CORE = '6a517b576baa17e7329661ee0476d1848081d114' MODEL_PROFILES = (MODEL, LARGE_MODEL) ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/tests/test_smoke_opencode_inference.py b/tests/test_smoke_opencode_inference.py index 4d12007..d8ef9f3 100644 --- a/tests/test_smoke_opencode_inference.py +++ b/tests/test_smoke_opencode_inference.py @@ -47,7 +47,7 @@ def test_exact_synthetic_inventory_is_bound_without_clean_git_claim(self): def test_profiles_are_closed_and_default_resources_are_unchanged(self): self.assertEqual(TRIAL.trial_profile(), dict(model_profile='qwen3-0.6b-v1', - core_revision='a5246942707890978867930dca518a29d1d48bd5', guest_memory_mib=6144, + core_revision='6a517b576baa17e7329661ee0476d1848081d114', guest_memory_mib=6144, core_memory_bytes=5 * TRIAL.GIB, qemu_memory_bytes=7 * TRIAL.GIB, host_available_bytes=8 * TRIAL.GIB, provision_budget_bytes=5 * TRIAL.GIB, scratch_gib=18, memory_failure='host_available_memory_below_8GiB')) @@ -59,7 +59,7 @@ def test_larger_profile_is_separate_and_requires_a_real_pin(self): self.assertRegex(TRIAL.LARGE_CORE, r'^[0-9a-f]{40}$') # One reviewed core supports both profiles and the new negotiated error # contract. Separate models/resources do not require artificial source forks. - self.assertEqual(TRIAL.LARGE_CORE, 'a5246942707890978867930dca518a29d1d48bd5') + self.assertEqual(TRIAL.LARGE_CORE, '6a517b576baa17e7329661ee0476d1848081d114') self.assertNotEqual(TRIAL.LARGE_MODEL, TRIAL.MODEL) with patch.object(TRIAL, 'LARGE_CORE', None): with self.assertRaisesRegex(ValueError, 'larger_core_not_pinned'): From dd7a709e0cc31351792d53fe0d826590719e6bb2 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:31:52 +0200 Subject: [PATCH 04/39] Accept exact owner-selected native CPU capabilities --- src/private-conversation.cjs | 12 ++++++- tests/private-conversation.test.cjs | 53 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/private-conversation.cjs b/src/private-conversation.cjs index 79663d8..29e197b 100644 --- a/src/private-conversation.cjs +++ b/src/private-conversation.cjs @@ -67,8 +67,18 @@ function capabilities(value, generationPolicyVersion, executionErrorVersion) { check(object(value)); const generation = generationPolicyVersion === 1 ? ['generation_policy_version', 'generation_policies'] : []; const execution = executionErrorVersion === 1 ? ['execution_error_version'] : []; + const backend = ['inference_backend', 'required_generation_policy']; equalLimits(value, { ...expectedLimits(value.model_profile), execution_slots: 1, - max_request_bytes: requestLimit(value.model_profile), max_response_bytes: 65536 }, ['max_seconds', 'quarantined', ...generation, ...execution]); + max_request_bytes: requestLimit(value.model_profile), max_response_bytes: 65536 }, + ['max_seconds', 'quarantined', ...generation, ...execution, ...backend]); + // The owner-selected core may expose this one backend restriction, not a + // frontend executor choice or independent proof of the returned backend. + if (backend.some(key => Object.hasOwn(value, key))) { + check(backend.every(key => Object.hasOwn(value, key)) && + value.inference_backend === 'llama_cpp_bf16_v1' && value.required_generation_policy === 'greedy_v1' && + value.model_profile === 'qwen3-4b-instruct-2507-v1' && generationPolicyVersion === 1, + 'incompatible_capabilities'); + } check(Number.isInteger(value.max_seconds) && value.max_seconds >= 1 && value.max_seconds <= 600 && typeof value.quarantined === 'boolean', 'incompatible_capabilities'); if (generationPolicyVersion === 1) { diff --git a/tests/private-conversation.test.cjs b/tests/private-conversation.test.cjs index 5a8b1fa..b10ef82 100644 --- a/tests/private-conversation.test.cjs +++ b/tests/private-conversation.test.cjs @@ -159,6 +159,59 @@ test('closed 4B profile binds exact template, limits, greedy policy and returned } }); +test('owner-selected native CPU capability pair preserves greedy socket requests and unchanged summaries', async t => { + // Framed socket contract only; no native backend, model or OpenCode is loaded. + const model = 'qwen3-4b-instruct-2507-v1'; + for (const backend of [{}, {inference_backend: 'llama_cpp_bf16_v1', required_generation_policy: 'greedy_v1'}]) { + const negotiated = {...caps(model), generation_policy_version: 1, generation_policies: ['greedy_v1'], + execution_error_version: 1, ...backend}; + const request = {...input(), generation_policy: 'greedy_v1'}; + const answer = {...result(undefined, model), generation_policy: 'greedy_v1'}; + const f = await fixture(t, (socket, message) => { + reply(socket, message, 'admitted'); reply(socket, message, 'result', {result: answer}); + }, structuredClone(negotiated), {generationPolicyVersion: 1, executionErrorVersion: 1}); + assert.deepEqual(await f.client.connect(), negotiated); + const returned = await f.client.submit(request); + assert.deepEqual(returned, answer); + assert.equal(Object.hasOwn(returned, 'inference_backend'), false); + assert.deepEqual(f.requests[0].operation, {type: 'conversation_capabilities', + generation_policy_version: 1, execution_error_version: 1}); + assert.deepEqual(f.requests[1].operation.conversation, request); + } +}); + +test('partial, unknown or unnegotiated native CPU capabilities fail before submission', async t => { + const model = 'qwen3-4b-instruct-2507-v1'; + const pair = {inference_backend: 'llama_cpp_bf16_v1', required_generation_policy: 'greedy_v1'}; + const negotiated = {...caps(model), generation_policy_version: 1, generation_policies: ['greedy_v1'], ...pair}; + const invalid = [ + {...negotiated, inference_backend: undefined}, + {...negotiated, required_generation_policy: undefined}, + {...negotiated, inference_backend: null}, + {...negotiated, inference_backend: 'llama_cpp_bf16_v2'}, + {...negotiated, required_generation_policy: null}, + {...negotiated, required_generation_policy: 'sampled'}, + {...negotiated, ...caps('qwen3-0.6b-v1')}, + {...negotiated, ...caps('smollm2-360m-v1')}, + {...negotiated, generation_policy_version: undefined}, + {...negotiated, generation_policy_version: 2}, + {...negotiated, generation_policies: []}, + {...negotiated, generation_policies: ['sampled']}, + {...negotiated, executable_path: '/untrusted/backend'}, + ]; + for (const advertised of invalid) { + const f = await fixture(t, () => assert.fail('invalid backend must not submit'), advertised, + {generationPolicyVersion: 1}); + await assert.rejects(f.client.connect()); + assert.equal(f.requests.length, 1); + } + for (const advertised of [{...caps(model), ...pair}, negotiated]) { + const f = await fixture(t, () => assert.fail('unnegotiated backend must not submit'), advertised); + await assert.rejects(f.client.connect()); + assert.equal(f.requests.length, 1); + } +}); + test('public/cloud/widened/unknown capabilities fail before any task', async t => { for (const change of [{ network_access: true }, { training: true }, { cloud_fallback: true }, { max_prompt_tokens: 999999 }, { native_tool_template: true }, { model_profile: 'unknown' }, From 6b0dfbdfbe37f9177e79e6aff3687018df83eff9 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:17:12 +0200 Subject: [PATCH 05/39] test: wire exact native CPU executor into disposable OpenCode trial --- .github/workflows/opencode-inference.yml | 22 +- docs/OPENCODE.md | 42 ++++ scripts/opencode_ci.py | 46 ++-- scripts/opencode_native_cpu.py | 146 +++++++++++ scripts/smoke_opencode_inference.py | 138 +++++++++-- tests/test_opencode_ci.py | 31 +++ tests/test_opencode_native_cpu.py | 296 +++++++++++++++++++++++ 7 files changed, 671 insertions(+), 50 deletions(-) create mode 100644 scripts/opencode_native_cpu.py create mode 100644 tests/test_opencode_native_cpu.py diff --git a/.github/workflows/opencode-inference.yml b/.github/workflows/opencode-inference.yml index 7bfae04..6811759 100644 --- a/.github/workflows/opencode-inference.yml +++ b/.github/workflows/opencode-inference.yml @@ -15,6 +15,14 @@ on: options: - qwen3-0.6b-v1 - qwen3-4b-instruct-2507-v1 + inference_backend: + description: Explicit owner-selected backend; native CPU requires the reviewed 4B profile + type: choice + required: true + default: torch + options: + - torch + - llama_cpp_bf16_v1 permissions: contents: read @@ -30,6 +38,7 @@ jobs: env: EXPECTED_CODE_SHA: ${{ inputs.expected_code_sha }} MODEL_PROFILE: ${{ inputs.model_profile }} + INFERENCE_BACKEND: ${{ inputs.inference_backend }} PYTHONDONTWRITEBYTECODE: '1' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -41,7 +50,7 @@ jobs: id: selected_core run: | set -euo pipefail - python3 -B scripts/opencode_ci.py select --model-profile "$MODEL_PROFILE" >>"$GITHUB_OUTPUT" + python3 -B scripts/opencode_ci.py select --model-profile "$MODEL_PROFILE" --inference-backend "$INFERENCE_BACKEND" >>"$GITHUB_OUTPUT" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: VOLPAROSSA/volparossa @@ -52,7 +61,7 @@ jobs: run: | set -euo pipefail python3 -B scripts/opencode_ci.py source --core "$PWD/build/ci-core" \ - --expected-code "$EXPECTED_CODE_SHA" --model-profile "$MODEL_PROFILE" + --expected-code "$EXPECTED_CODE_SHA" --model-profile "$MODEL_PROFILE" --inference-backend "$INFERENCE_BACKEND" - name: Install official tools only on the disposable GitHub host run: | set -euo pipefail @@ -81,14 +90,14 @@ jobs: export XDG_RUNTIME_DIR export DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus" printf 'XDG_RUNTIME_DIR=%s\nDBUS_SESSION_BUS_ADDRESS=%s\n' "$XDG_RUNTIME_DIR" "$DBUS_SESSION_BUS_ADDRESS" >>"$GITHUB_ENV" - python3 -B scripts/opencode_ci.py preflight --model-profile "$MODEL_PROFILE" + python3 -B scripts/opencode_ci.py preflight --model-profile "$MODEL_PROFILE" --inference-backend "$INFERENCE_BACKEND" - name: Source-build the pinned OpenCode runtime (no model) timeout-minutes: 55 run: bash scripts/opencode_ci_build.sh - name: Fetch exact public Node and Debian guest image run: | set -euo pipefail - python3 -B scripts/opencode_ci.py assets --core "$PWD/build/ci-core" --model-profile "$MODEL_PROFILE" + python3 -B scripts/opencode_ci.py assets --core "$PWD/build/ci-core" --model-profile "$MODEL_PROFILE" --inference-backend "$INFERENCE_BACKEND" image_url=$(jq -er '.url' build/ci-core/tests/helper/debian13-amd64-image-v1.json) image="$PWD/build/debian-13-genericcloud-amd64-20260826-2582.qcow2" curl --fail --silent --show-error --location --connect-timeout 30 --max-time 1200 \ @@ -102,8 +111,8 @@ jobs: test "$(git rev-parse HEAD)" = "$EXPECTED_CODE_SHA" test -z "$(git status --porcelain)" python3 -B scripts/smoke_opencode_inference.py pack --core "$PWD/build/ci-core" \ - --node "$PWD/build/ci-node/bin/node" --output "$PWD/build/ci-inputs.tar.gz" --model-profile "$MODEL_PROFILE" - python3 -B scripts/opencode_ci.py capture --model-profile "$MODEL_PROFILE" + --node "$PWD/build/ci-node/bin/node" --output "$PWD/build/ci-inputs.tar.gz" --model-profile "$MODEL_PROFILE" --inference-backend "$INFERENCE_BACKEND" + python3 -B scripts/opencode_ci.py capture --model-profile "$MODEL_PROFILE" --inference-backend "$INFERENCE_BACKEND" - name: One actual OpenCode core Qwen edit and test trial timeout-minutes: 115 run: | @@ -115,6 +124,7 @@ jobs: XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \ python3 -B scripts/smoke_opencode_inference.py execute --yes --host-tools-profile github-ubuntu-24.04 \ --model-profile "$MODEL_PROFILE" \ + --inference-backend "$INFERENCE_BACKEND" \ --core "$PWD/build/ci-core" --tools /usr \ --image "$PWD/build/debian-13-genericcloud-amd64-20260826-2582.qcow2" \ --bundle "$PWD/build/ci-inputs.tar.gz" --output "$PWD/build/ci-vm" diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 95b7547..d56f388 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -708,3 +708,45 @@ the default branch. Dispatch it on `feature/opencode-integration`, supplying that exact reviewed commit as `expected_code_sha`; dispatching an unprepared main branch cannot pass the source guard and must not install tools or launch a guest. There is no automatic inference run on push or pull request. + +### Explicit native CPU candidate + +The later original [4B trial 37228603308](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37228603308) +on Code `714904a3ec452b8e10d8b40be6ac85107606ca3b` and core +`6a517b576baa17e7329661ee0476d1848081d114` remains **failed**. One request +reached the first model-forward start with 4,572 prompt tokens, then exhausted +its execution budget without a completed forward, token, EOS, edit or test. +The negotiated terminal error stopped blind retries. Cleanup was confirmed; +the observations do not prove that all weights became resident or identify the +stall's cause. Original artifact ZIP SHA-256: +`fabcc8ba08f47f28f85614b645679654b70f83f02c85c4f8be8722c0d1d5978c`. + +An explicitly selected `--inference-backend llama_cpp_bf16_v1` now prepares a +different CPU executor for that same original 4B task. It pins core +`f7e2c3b9c2abd710682a2ec51d4b5dfbc46b9d8f` and llama.cpp source +`7fe450e19305b828c199d602c23a8337aaa1f03b`. The default `torch` backend and +both existing model profiles retain their previous core pins. Native selection +is refused for the smaller profile; there is no silent backend fallback. + +Select the native backend consistently for source selection, packing and +execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow +exposes the same explicit choice. It must be registered and dispatched against +the exact reviewed Code commit; a source pin is not evidence of a successful run. + +Only the disposable guest fetches and compiles the pinned native source. The +new source-build service has two build jobs, low priority, the existing 11 GiB +guest-service limit, no swap and a 1,800-second ceiling; the outer VM/SSH budgets +are unchanged. Conversion stays within the original 20 GiB/1,800-second +provisioning budget. It adds one pinned converter dependency, verifies all +original tensor values and authorizes the measured backend manifest explicitly. +The original model, full prompt, greedy policy, two worker threads, 600-second +request budget, coding task and independent tests are unchanged. This is not +quantization, a smaller model, confidential remote execution or a new app-level +peer scheduler. + +Forty-three targeted Python fixture checks and forty-seven Node consumer/session +checks pass. They cover exact source/backend/artifact bindings, negative cases, +unchanged defaults, measured resource limits and cleanup contracts. No actual +conversion, native model inference or successful coding loop has yet been proved. +The core's retained full upstream sanitizer failure on a disallowed quantized +path also remains explicit; separate BF16/F32 checks do not erase that failure. diff --git a/scripts/opencode_ci.py b/scripts/opencode_ci.py index 23b4647..14d8a19 100644 --- a/scripts/opencode_ci.py +++ b/scripts/opencode_ci.py @@ -25,6 +25,7 @@ PROFILE = 'github-ubuntu-24.04' MODEL = 'qwen3-0.6b-v1' MODEL_PROFILES = (MODEL, 'qwen3-4b-instruct-2507-v1') +INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') TOOLS = {'qemu-system-x86_64': ('/usr/bin/qemu-system-x86_64', 'qemu-system-x86'), 'qemu-img': ('/usr/bin/qemu-img', 'qemu-utils'), 'cloud-localds': ('/usr/bin/cloud-localds', 'cloud-image-utils'), @@ -75,14 +76,18 @@ def guard(): and os.uname().machine == 'x86_64', 'ubuntu_24_amd64_only') -def trial_profile(model_profile=MODEL): +def trial_profile(model_profile=MODEL, inference_backend='torch'): trial = module(ROOT / 'scripts/smoke_opencode_inference.py', 'ci_trial_profile') - return trial.trial_profile(model_profile) + return trial.trial_profile(model_profile, inference_backend) -def exact_sources(core, expected, model_profile=MODEL): +def backend_fields(profile): + return {key: profile[key] for key in ('inference_backend', 'native_source_commit') if key in profile} + + +def exact_sources(core, expected, model_profile=MODEL, inference_backend='torch'): guard() - profile = trial_profile(model_profile) + profile = trial_profile(model_profile, inference_backend) require(re.fullmatch('[0-9a-f]{40}', expected or '') and expected == os.environ.get('GITHUB_SHA'), 'exact_dispatched_source') require(core.resolve(strict=True) == core and core == BUILD / 'ci-core', 'core_checkout_scope') @@ -94,7 +99,7 @@ def exact_sources(core, expected, model_profile=MODEL): return {'version': 1, 'code_revision': expected, 'code_baseline': BASELINE, 'code_tree': run(['git', '-C', ROOT, 'rev-parse', 'HEAD^{tree}'], text=True).stdout.strip(), 'core_revision': profile['core_revision'], 'model_profile': model_profile, 'code_checkout_clean': True, - 'host_tools_profile': PROFILE, 'actual_inference_proven': False} + 'host_tools_profile': PROFILE, 'actual_inference_proven': False, **backend_fields(profile)} def verify_host_tools(tools): @@ -117,10 +122,10 @@ def verify_host_tools(tools): 'source_built_host_tools': False, 'debian_workspace_pins_claimed': False} -def preflight(model_profile=MODEL): +def preflight(model_profile=MODEL, inference_backend='torch'): tools = verify_host_tools(Path('/usr')) trial = module(ROOT / 'scripts/smoke_opencode_inference.py', 'ci_trial_preflight') - profile = trial.trial_profile(model_profile) + profile = trial.trial_profile(model_profile, inference_backend) require(trial.available_memory() >= profile['host_available_bytes'], profile['memory_failure']) name = 'volparossa-opencode-preflight-' + uuid.uuid4().hex[:12] + '.service' # The service itself, not merely the invoking shell, proves KVM access and @@ -156,7 +161,7 @@ def preflight(model_profile=MODEL): record(BUILD / 'ci-preflight.json', {'version': 1, 'passed': True, 'actual_kvm_api': 12, 'actual_user_cgroup': True, 'model_profile': model_profile, 'memory_max': profile['qemu_memory_bytes'], 'host_available_required': profile['host_available_bytes'], 'swap_max': 0, - 'preflight_service_joined': True, 'vm_started': False}) + 'preflight_service_joined': True, 'vm_started': False, **backend_fields(profile)}) def source_build(): @@ -188,9 +193,9 @@ def source_build(): record(BUILD / 'ci-build.json', value) -def assets(core, model_profile=MODEL): +def assets(core, model_profile=MODEL, inference_backend='torch'): guard() - profile = trial_profile(model_profile) + profile = trial_profile(model_profile, inference_backend) require(run(['git', '-C', core, 'rev-parse', 'HEAD'], text=True).stdout.strip() == profile['core_revision'], 'exact_core') private = module(core / 'tests/integration/agent-private-conversation.py', 'ci_node_assets') @@ -200,19 +205,20 @@ def assets(core, model_profile=MODEL): private.extract_node(archive, BUILD / 'ci-node', pin['files']) -def capture(model_profile=MODEL): +def capture(model_profile=MODEL, inference_backend='torch'): guard() trial = module(ROOT / 'scripts/smoke_opencode_inference.py', 'ci_trial_inputs') - profile = trial.trial_profile(model_profile) + profile = trial.trial_profile(model_profile, inference_backend) path = BUILD / 'ci-inputs.tar.gz' - manifest = trial.validate_bundle(path, model_profile) + manifest = trial.validate_bundle(path, model_profile, inference_backend) source = json.loads((BUILD / 'ci-source.json').read_text()) + trial.validate_backend_fields(source, profile) require(source['code_revision'] == manifest['code_base_revision'] == os.environ['GITHUB_SHA'], 'input_source') require(source['core_revision'] == manifest['core_revision'] == profile['core_revision'] and source['model_profile'] == manifest['model_profile'] == model_profile, 'input_profile') record(BUILD / 'ci-inputs.json', {'version': 1, 'bundle_sha256': digest(path), 'code_revision': source['code_revision'], 'core_revision': profile['core_revision'], 'model_profile': model_profile, - 'code_checkout_clean': True, 'input_manifest': manifest}) + 'code_checkout_clean': True, 'input_manifest': manifest, **backend_fields(profile)}) def export(): @@ -240,20 +246,21 @@ def main(): parser.add_argument('--core', type=Path) parser.add_argument('--expected-code') parser.add_argument('--model-profile', choices=MODEL_PROFILES, default=MODEL) + parser.add_argument('--inference-backend', choices=INFERENCE_BACKENDS, default='torch') args = parser.parse_args() if args.mode == 'guard': guard() elif args.mode == 'select': guard() - print('core_revision=' + trial_profile(args.model_profile)['core_revision']) + print('core_revision=' + trial_profile(args.model_profile, args.inference_backend)['core_revision']) elif args.mode == 'source': - record(BUILD / 'ci-source.json', exact_sources(args.core, args.expected_code, args.model_profile)) + record(BUILD / 'ci-source.json', exact_sources(args.core, args.expected_code, args.model_profile, args.inference_backend)) elif args.mode == 'assets': - assets(args.core, args.model_profile) + assets(args.core, args.model_profile, args.inference_backend) elif args.mode == 'preflight': - preflight(args.model_profile) + preflight(args.model_profile, args.inference_backend) elif args.mode == 'capture': - capture(args.model_profile) + capture(args.model_profile, args.inference_backend) else: {'build': source_build, 'export': export}[args.mode]() @@ -267,6 +274,7 @@ def main(): 'official_package_owner', 'package_integrity', 'package_version', 'host_available_memory_below_8GiB', 'host_available_memory_below_14GiB', 'preflight_stop', 'preflight_observation', 'unknown_model_profile', 'larger_core_not_pinned', + 'unknown_inference_backend', 'native_backend_requires_qwen4b', 'native_core_not_pinned', 'input_backend', 'preflight_cleanup', 'exact_core', 'input_source', 'input_profile', 'closed_receipt_file', 'closed_receipt_object'} reason = error.args[0] if error.args and isinstance(error.args[0], str) else None diff --git a/scripts/opencode_native_cpu.py b/scripts/opencode_native_cpu.py new file mode 100644 index 0000000..867905d --- /dev/null +++ b/scripts/opencode_native_cpu.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-3.0-only +"""Guest-only source build and closed native provenance for the original trial. + +No model is loaded here. The existing core provisioner owns conversion inside +its original disk/deadline budget; core separately verifies artifacts before use. +""" +import argparse +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import pwd +import re +import socket +import stat +import subprocess + +SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' +ORIGIN = 'https://github.com/ggml-org/llama.cpp.git' +KIND = 'llama_cpp_bf16_v1' +MODEL = 'qwen3-4b-instruct-2507-v1' +BASE = Path('/home/vpci/opencode-trial') +CORE = Path('/home/vpci/source') +ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1', + 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': '/dev/null', 'GIT_TERMINAL_PROMPT': '0'} + + +def require(value, reason): + if not value: + raise ValueError(reason) + + +def module(path, name): + spec = importlib.util.spec_from_file_location(name, path) + value = importlib.util.module_from_spec(spec) + spec.loader.exec_module(value) + return value + + +def file_identity(path, maximum): + with os.fdopen(os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK), 'rb') as stream: + info = os.fstat(stream.fileno()) + require(stat.S_ISREG(info.st_mode) and info.st_uid == os.getuid() and info.st_nlink == 1 + and 0 < info.st_size <= maximum, 'native_file') + return {'bytes': info.st_size, 'sha256': hashlib.file_digest(stream, 'sha256').hexdigest()} + + +def document(path, maximum): + file_identity(path, maximum) + raw = path.read_bytes() + require(len(raw) <= maximum, 'native_document_bound') + return json.loads(raw), hashlib.sha256(raw).hexdigest() + + +def validate_build(core, base): + builder = module(core / 'workers/volparossa-ml/build_llama_cpu.py', 'trial_native_builder') + require(builder.SOURCE == SOURCE and builder.ORIGIN == ORIGIN and builder.KIND == KIND, 'native_builder_pin') + tree = builder.verify_source(base / 'llama-source') + value, report_hash = document(base / 'llama-build/build.json', 65536) + require(value.get('version') == 1 and value.get('kind') == KIND and value.get('source_commit') == SOURCE + and value.get('source_tree') == tree and value.get('abi_version') == 1 + and value.get('jobs') == 2 and value.get('cpu') == 'avx2_fma_f16c' + and value.get('models_loaded') is False and value.get('downloads') is False + and value.get('install') is False and value.get('sanitizers') is False + and value.get('provisionable') is True and value.get('quantization') is False, + 'native_build_provenance') + library = file_identity(base / 'llama-build/libvolparossa_llama_cpu.so', 128 * 1024 ** 2) + require(value.get('library') == {'path': 'libvolparossa_llama_cpu.so', **library}, 'native_library_binding') + wrapper = {name: file_identity(core / 'workers/volparossa-ml/native-cpu' / name, 1024 ** 2) + for name in ('CMakeLists.txt', 'adapter.h', 'adapter.cpp')} + require(value.get('wrapper_sources') == wrapper, 'native_wrapper_binding') + require(type(value.get('dynamic_dependencies')) is list + and value['dynamic_dependencies'] and len(set(value['dynamic_dependencies'])) == len(value['dynamic_dependencies']) + and set(value['dynamic_dependencies']) <= builder.ALLOWED_NEEDED, 'native_dependency_binding') + return dict(kind=KIND, source_commit=SOURCE, source_tree=tree, build_manifest_sha256=report_hash, + library=library, wrapper_sources=wrapper, jobs=2, nice=19, + cpu='avx2_fma_f16c', quantization=False, models_loaded=False) + + +def validate_provision(core, base, report, build): + require(report.get('native_cpu_converter') == {'implementation': KIND, + 'dependencies': {'sentencepiece': '0.2.1'}}, 'native_converter_binding') + backend = report.get('native_backend') + require(type(backend) is dict and set(backend) == {'kind', 'root', 'backend_sha256'} + and backend['kind'] == KIND and backend['root'] == str(base / 'ml/native-backend') + and type(backend['backend_sha256']) is str and re.fullmatch('[0-9a-f]{64}', backend['backend_sha256']), + 'native_provision_binding') + root = base / 'ml/native-backend' + require(root.resolve(strict=True) == root and stat.S_IMODE(root.stat().st_mode) == 0o700, 'native_backend_root') + value, manifest_hash = document(root / 'backend.json', 16384) + native = module(core / 'workers/volparossa-ml/llama_cpu.py', 'trial_native_contract') + native.validate_manifest(value, require) + require(manifest_hash == backend['backend_sha256'] and value['model_profile'] == MODEL + and value['build_manifest_sha256'] == build['build_manifest_sha256'] + and value['library'] == {'path': 'libvolparossa_llama_cpu.so', **build['library']}, 'native_manifest_binding') + _, build_hash = document(root / 'build.json', 65536) + require(build_hash == build['build_manifest_sha256'], 'native_copied_build_binding') + for name, maximum in (('library', 128 * 1024 ** 2), ('gguf', 9 * 1024 ** 3)): + require(value[name] == {'path': value[name]['path'], **file_identity(root / value[name]['path'], maximum)}, + 'native_artifact_binding') + # Closed hashes/sizes only: no host paths, prompts, source code or model text. + return dict(kind=KIND, source_commit=SOURCE, manifest_sha256=manifest_hash, + build_manifest_sha256=build_hash, library_sha256=value['library']['sha256'], + gguf_sha256=value['gguf']['sha256'], gguf_bytes=value['gguf']['bytes'], + source_weights_sha256=value['source_weights_sha256']) + + +def guest_build(): + require(os.getuid() > 0 and pwd.getpwuid(os.getuid()).pw_name == 'vpci' + and socket.gethostname() == 'volparossa-alpha' + and subprocess.run(['systemd-detect-virt', '--vm'], check=True, capture_output=True, + text=True, timeout=10).stdout.strip() == 'kvm' + and 'VERSION_ID="13"' in Path('/etc/os-release').read_text(), 'disposable_guest_required') + os.umask(0o077) + source, output = BASE / 'llama-source', BASE / 'llama-build' + require(not source.exists() and not source.is_symlink() and not output.exists() and not output.is_symlink(), + 'native_fresh_directories') + source.mkdir(mode=0o700) + git = ['git', '-c', 'credential.helper=', '-c', 'protocol.file.allow=never', '-c', 'protocol.ext.allow=never'] + for operation in (['init', '--quiet'], ['remote', 'add', 'origin', ORIGIN], + ['fetch', '--depth=1', '--no-tags', 'origin', SOURCE], ['checkout', '--detach', '--quiet', 'FETCH_HEAD']): + subprocess.run([*git, *operation], cwd=source, env=ENV, check=True, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=300) + builder = module(CORE / 'workers/volparossa-ml/build_llama_cpu.py', 'trial_native_build') + require(builder.SOURCE == SOURCE and builder.ORIGIN == ORIGIN, 'native_builder_pin') + builder.verify_source(source) + # This whole helper is inside the single 1800-second guest cgroup. The + # original builder independently fixes max two jobs and nice19, and owns + # compiler cleanup. No source acquisition or compilation runs on the host. + subprocess.run([os.sys.executable, '-B', str(CORE / 'workers/volparossa-ml/build_llama_cpu.py'), + '--source', str(source), '--output', str(output), '--execute'], + env=ENV, check=True, timeout=1800) + validate_build(CORE, BASE) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--execute', action='store_true', required=True) + parser.parse_args() + try: + guest_build() + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError): + print('NATIVE_GUEST_SOURCE_BUILD_FAILED') + raise SystemExit(1) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index e4adbfc..4a44366 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -34,6 +34,10 @@ # Both profiles require the reviewed terminal-budget IPC; model and budgets stay distinct. LARGE_CORE = '6a517b576baa17e7329661ee0476d1848081d114' MODEL_PROFILES = (MODEL, LARGE_MODEL) +INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') +# Separately reviewed native converter dependency and process-group cleanup. +NATIVE_CORE = 'f7e2c3b9c2abd710682a2ec51d4b5dfbc46b9d8f' +NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' IMAGE_SHA512 = '184761b0dad0f9ace02f9298050ca96ce3caa39a461a47706d47ff9698b59933918b91b40177fbd4d392f6446af8b4d18ecb94caca988169b19641606bf34003' @@ -43,6 +47,7 @@ PROJECTS = Path('/home/vpci/opencode-projects') CORE_UNIT = 'volparossa-opencode-core.service' TASK_UNIT = 'volparossa-opencode-task.service' +NATIVE_UNIT = 'volparossa-opencode-native-build.service' def require(value, reason): @@ -50,9 +55,15 @@ def require(value, reason): raise ValueError(reason) -def trial_profile(model_profile=MODEL): +def trial_profile(model_profile=MODEL, inference_backend='torch'): """Closed source/resource choices; the larger trial never changes the default.""" require(model_profile in MODEL_PROFILES, 'unknown_model_profile') + require(inference_backend in INFERENCE_BACKENDS, 'unknown_inference_backend') + if inference_backend != 'torch': + require(model_profile == LARGE_MODEL, 'native_backend_requires_qwen4b') + require(type(NATIVE_CORE) is str and re.fullmatch('[0-9a-f]{40}', NATIVE_CORE), 'native_core_not_pinned') + return dict(trial_profile(model_profile), core_revision=NATIVE_CORE, + inference_backend=inference_backend, native_source_commit=NATIVE_SOURCE) if model_profile == MODEL: return dict(model_profile=MODEL, core_revision=CORE, guest_memory_mib=6144, core_memory_bytes=5 * GIB, qemu_memory_bytes=7 * GIB, @@ -66,6 +77,16 @@ def trial_profile(model_profile=MODEL): scratch_gib=40, memory_failure='host_available_memory_below_14GiB') +def backend_fields(profile): + return {key: profile[key] for key in ('inference_backend', 'native_source_commit') if key in profile} + + +def validate_backend_fields(value, profile): + expected = backend_fields(profile) + require({key: value[key] for key in ('inference_backend', 'native_source_commit') if key in value} + == expected, 'input_backend') + + def digest(path, algorithm='sha256'): with path.open('rb') as stream: return hashlib.file_digest(stream, algorithm).hexdigest() @@ -89,7 +110,7 @@ def load(path, maximum=2 * 1024**2): return json.loads(path.read_bytes()) -def closed_provision(path, pins, stage, returncode, wait_timeout=False): +def closed_provision(path, pins, stage, returncode, wait_timeout=False, native_cpu=False): """Bounded metadata only; progress means download starts, not verified assets.""" stages = {'pins', 'launch', 'process', 'report', 'provenance', 'complete'} value = dict(version=1, stage=stage if stage in stages else 'unknown', @@ -97,6 +118,7 @@ def closed_provision(path, pins, stage, returncode, wait_timeout=False): wait_timeout=wait_timeout is True, log_state='absent', progress_state='no_signal', download_starts=0, last_artifact_index=None, failure_class='unknown', http_status=None, wheel_graph_checked=False, runtime_import_checked=False) + native_conversion_failed = False try: descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) with os.fdopen(descriptor, 'rb') as stream: @@ -127,6 +149,7 @@ def closed_provision(path, pins, stage, returncode, wait_timeout=False): value.update(progress_state='ordered', download_starts=index + 1, last_artifact_index=index) if value['log_state'] != 'present': continue + native_conversion_failed |= native_cpu is True and line == b'NATIVE_CONVERSION_FAILED' value['wheel_graph_checked'] |= line == b'PINNED_WHEEL_GRAPH_OK' value['runtime_import_checked'] |= line == b'OFFLINE_CPU_RUNTIME_IMPORT_OK' prefix = b'Provisioning refused: ' @@ -165,6 +188,8 @@ def closed_provision(path, pins, stage, returncode, wait_timeout=False): pass except (OSError, ValueError, KeyError, TypeError): value['log_state'] = 'invalid' + if native_conversion_failed and value['log_state'] == 'present': + value['failure_class'] = 'native_conversion' return value @@ -187,7 +212,7 @@ def new_output(path): def pack(args): - profile = trial_profile(getattr(args, 'model_profile', MODEL)) + profile = trial_profile(getattr(args, 'model_profile', MODEL), getattr(args, 'inference_backend', 'torch')) new_output(args.output) canonical(args.core) require(run(['git', '-C', args.core, 'rev-parse', 'HEAD'], text=True).stdout.strip() @@ -204,6 +229,7 @@ def pack(args): files = {f'code/{path.relative_to(ROOT)}': path for path in sorted((ROOT / 'src').glob('*.cjs'))} names = ['src/opencode-cooperative-tool.js', 'scripts/opencode_session.py', 'scripts/opencode_session.cjs', 'scripts/smoke_opencode_inference.cjs', 'scripts/smoke_opencode_inference.py', + 'scripts/opencode_native_cpu.py', 'third_party/opencode.json', 'third_party/opencode-LICENSE.txt', 'third_party/opencode-build-tools.json', 'patches/opencode-no-runtime-installs.patch', 'LICENSE', 'THIRD_PARTY_LICENSES.md'] @@ -228,6 +254,7 @@ def pack(args): code_contains_uncommitted_changes=True, code_git_head_proves_migration=False, node_version='24.19.0', model_profile=profile['model_profile'], opencode_revision=report['source_commit'], opencode_binary_sha256=report['binary_sha256'], files=inventory) + manifest.update(backend_fields(profile)) encoded = (json.dumps(manifest, sort_keys=True, indent=2) + '\n').encode() with tarfile.open(args.output, 'x:gz', compresslevel=1) as target: for name, source in files.items(): @@ -247,9 +274,10 @@ def pack(args): 'code_contains_uncommitted_changes': True})) -def staged_inputs(model_profile=MODEL): - profile = trial_profile(model_profile) +def staged_inputs(model_profile=MODEL, inference_backend='torch'): + profile = trial_profile(model_profile, inference_backend) manifest = load(INPUT / 'INPUTS.json') + validate_backend_fields(manifest, profile) require(manifest['core_revision'] == profile['core_revision'] and manifest['model_profile'] == model_profile and manifest['code_contains_uncommitted_changes'] is True and manifest['code_git_head_proves_migration'] is False, 'input_authority') @@ -265,7 +293,7 @@ def staged_inputs(model_profile=MODEL): def unit(name, *args, check=True): - require(name in (CORE_UNIT, TASK_UNIT), 'unit_scope') + require(name in (CORE_UNIT, TASK_UNIT, NATIVE_UNIT), 'unit_scope') return subprocess.run(['sudo', '-n', 'systemctl', *args, name], capture_output=True, text=True, timeout=20, check=check) @@ -277,7 +305,7 @@ def properties(name): def cgroup(name): - require(name in (CORE_UNIT, TASK_UNIT), 'cgroup_scope') + require(name in (CORE_UNIT, TASK_UNIT, NATIVE_UNIT), 'cgroup_scope') return Path('/sys/fs/cgroup/system.slice') / name @@ -294,28 +322,50 @@ def memory(name): 'oom_kill': int(events['oom_kill'])} -def start(name, command, limit, seconds): +def closed_native_unit(state): + code = state.get('ExecMainStatus', '') + return dict(active=state.get('ActiveState') if state.get('ActiveState') in + ('active', 'inactive', 'failed', 'activating', 'deactivating') else 'unknown', + result=state.get('Result') if state.get('Result') in + ('success', 'exit-code', 'signal', 'core-dump', 'oom-kill', 'timeout', 'resources') else 'unknown', + status=int(code) if re.fullmatch('[0-9]{1,3}', code) and int(code) <= 255 else None) + + +def native_memory(limit): + value = memory(NATIVE_UNIT) + group = cgroup(NATIVE_UNIT) + events = dict(row.split() for row in (group / 'memory.events').read_text().splitlines()) + value.update(swap_current=int((group / 'memory.swap.current').read_text()), oom=int(events['oom'])) + require(value['maximum'] == limit and 0 < value['peak'] <= limit + and value['swap'] == value['swap_current'] == value['oom'] == value['oom_kill'] == 0, + 'native_build_resource_violation') + return value + + +def start(name, command, limit, seconds, public_source_build=False): + require(not public_source_build or name == NATIVE_UNIT, 'native_build_network_scope') (BASE / (name + '.log')).touch(mode=0o600) run(['sudo', '-n', 'systemd-run', '--quiet', '--unit=' + name, '--property=Type=exec', '--property=RemainAfterExit=yes', '--property=User=vpci', '--property=Group=vpci', '--property=WorkingDirectory=/home/vpci/source', '--property=MemoryMax=' + str(limit), '--property=MemorySwapMax=0', '--property=TasksMax=256', '--property=KillMode=control-group', '--property=RuntimeMaxSec=' + str(seconds), '--property=TimeoutStopSec=15', - '--property=NoNewPrivileges=yes', '--property=PrivateNetwork=yes', + '--property=NoNewPrivileges=yes', '--property=PrivateNetwork=' + ('no' if public_source_build else 'yes'), '--property=StandardOutput=append:' + str(BASE / (name + '.log')), '--property=StandardError=append:' + str(BASE / (name + '.log')), '--setenv=RUST_LOG=off,volparossa::compute::private_diagnostic=debug', '--setenv=NO_COLOR=1', *command]) def guest(args): - profile = trial_profile(getattr(args, 'model_profile', MODEL)) + backend = getattr(args, 'inference_backend', 'torch') + profile = trial_profile(getattr(args, 'model_profile', MODEL), backend) os.umask(0o077) require(os.getuid() > 0 and pwd.getpwuid(os.getuid()).pw_name == 'vpci' and socket.gethostname() == 'volparossa-alpha' and run(['systemd-detect-virt', '--vm'], text=True).stdout.strip() == 'kvm' and 'VERSION_ID="13"' in Path('/etc/os-release').read_text(), 'disposable_guest_required') require(not BASE.exists() and not PROJECTS.exists() and not SOURCE.exists(), 'fresh_guest') - manifest = staged_inputs(profile['model_profile']) + manifest = staged_inputs(profile['model_profile'], backend) output = Path('/home/vpci/opencode-result.json') require(not output.exists(), 'fresh_receipt') BASE.mkdir(mode=0o700) @@ -333,6 +383,7 @@ def guest(args): code_contains_uncommitted_changes=True, model_profile=profile['model_profile'], actual_model_provisioned=False, private_peer_execution_proven=False, confidential_remote_execution_proven=False, raw_model_output_exported=False, host_state_unchanged=None, units_empty=False, private_data_removed=False) + report.update(backend_fields(profile)) try: for name in (CORE_UNIT, TASK_UNIT): require(properties(name).get('LoadState') == 'not-found', 'existing_unit') @@ -350,15 +401,36 @@ def guest(args): subprocess.run(['/usr/bin/cargo', 'build', '--locked', '-p', 'volparossa', '--bin', 'volparossa'], cwd=SOURCE, env=build_env, stdout=log, stderr=log, timeout=1200, check=True) report['core_binary_sha256'] = digest(private.CLI) + if backend != 'torch': + report['phase'] = 'native-source-build' + require(properties(NATIVE_UNIT).get('LoadState') == 'not-found', 'existing_unit') + created.append(NATIVE_UNIT) + start(NATIVE_UNIT, [sys.executable, '-B', str(INPUT / 'code/scripts/opencode_native_cpu.py'), + '--execute'], profile['core_memory_bytes'], 1800, public_source_build=True) + deadline = time.monotonic() + 1815 + while time.monotonic() < deadline: + native_state = properties(NATIVE_UNIT) + if native_state.get('SubState') == 'exited' or native_state.get('ActiveState') in ('failed', 'inactive'): + break + time.sleep(2) + report['native_build_unit'] = closed_native_unit(native_state) + require(native_state.get('SubState') == 'exited' and native_state.get('Result') == 'success' + and native_state.get('ExecMainStatus') == '0' and empty(NATIVE_UNIT), 'native_build_failed') + report['native_build_memory'] = native_memory(profile['core_memory_bytes']) + native = module(INPUT / 'code/scripts/opencode_native_cpu.py', 'opencode_native_inputs') + report['native_build'] = native.validate_build(SOURCE, BASE) report['phase'] = 'model-provision' provision_stage = 'pins' model = module(private.ML / 'provision.py', 'opencode_model_provision') - provision_pins = model.load_pins(profile['model_profile']) + provision_pins = (model.load_pins(profile['model_profile'], native_cpu_converter=True) + if backend != 'torch' else model.load_pins(profile['model_profile'])) provision_stage = 'launch' with (BASE / 'provision.log').open('xb') as log: provision = subprocess.Popen([sys.executable, '-B', str(private.ML / 'provision.py'), '--execute', '--yes', '--disposable-guest', '--model-profile', profile['model_profile'], - '--root', str(BASE / 'ml'), '--budget-bytes', str(profile['provision_budget_bytes'])], + '--root', str(BASE / 'ml'), '--budget-bytes', str(profile['provision_budget_bytes']), + *(['--native-cpu-source', str(BASE / 'llama-source'), '--native-cpu-build', str(BASE / 'llama-build')] + if backend != 'torch' else [])], stdout=log, stderr=log, start_new_session=True, env=ENV) provision_stage = 'process' try: @@ -378,6 +450,9 @@ def guest(args): and observed['runtime_autofetch_enabled'] is False and {key: observed[key] for key in expected} == expected, 'model_provenance') report['actual_model_provisioned'], report['model_provision'] = True, expected + if backend != 'torch': + native = module(INPUT / 'code/scripts/opencode_native_cpu.py', 'opencode_native_provision') + report['native_backend'] = native.validate_provision(SOURCE, BASE, observed, report['native_build']) provision_stage = 'complete' report['phase'] = 'core-start' (BASE / 'work').mkdir(mode=0o700) @@ -385,7 +460,9 @@ def guest(args): start(CORE_UNIT, [str(private.CLI), 'compute', 'private-serve', '--socket', str(BASE / 'private.sock'), '--work-parent', str(BASE / 'work'), '--runtime-root', str(BASE / 'ml/venv'), '--model-root', str(BASE / 'ml/model'), '--model-profile', profile['model_profile'], - '--threads', '2', '--max-seconds', '600', '--execute'], profile['core_memory_bytes'], 2700) + '--threads', '2', '--max-seconds', '600', '--execute', + *(['--native-backend-root', str(BASE / 'ml/native-backend'), '--native-backend-sha256', + report['native_backend']['manifest_sha256']] if backend != 'torch' else [])], profile['core_memory_bytes'], 2700) deadline = time.monotonic() + 15 while not (BASE / 'private.sock').exists() and time.monotonic() < deadline: time.sleep(.1) @@ -416,7 +493,7 @@ def guest(args): report['core_diagnostics'] = private.service_diagnostic(BASE / (CORE_UNIT + '.log')) require(report['task_exit_status'] == 0 and report.get('task', {}).get('passed') is True, 'task_failed') require(empty(TASK_UNIT) and not list((BASE / 'work').iterdir()), 'task_private_cleanup') - require(staged_inputs(profile['model_profile']) == manifest, 'staged_inputs_changed') + require(staged_inputs(profile['model_profile'], backend) == manifest, 'staged_inputs_changed') report['inputs_unchanged'] = True report['phase'] = 'core-stop' unit(CORE_UNIT, 'kill', '--kill-whom=main', '--signal=SIGINT') @@ -436,13 +513,16 @@ def guest(args): report['provision_group_joined'] = private.stop_client(provision) if provision_stage is not None: report['model_provision_diagnostic'] = closed_provision(BASE / 'provision.log', provision_pins, - provision_stage, provision.returncode if provision is not None else None, provision_wait_timeout) + provision_stage, provision.returncode if provision is not None else None, provision_wait_timeout, + native_cpu=backend != 'torch') # The original worker monitor owns descendants; stopping the complete # cgroup joins nested native/model processes, not just their leaders. for name in reversed(created): if cgroup(name).exists(): try: - report['core_memory' if name == CORE_UNIT else 'task_memory'] = memory(name) + report['core_memory' if name == CORE_UNIT else + 'native_build_memory' if name == NATIVE_UNIT else 'task_memory'] = ( + native_memory(profile['core_memory_bytes']) if name == NATIVE_UNIT else memory(name)) except (OSError, ValueError, KeyError): report['failure'] = 'resource_observation_failed' unit(name, 'stop', check=False) @@ -560,8 +640,8 @@ def boot_running(state): and int(state['MainPID']) > 0 -def qemu_command(tools, scratch, firmware=None, model_profile=MODEL): - profile = trial_profile(model_profile) +def qemu_command(tools, scratch, firmware=None, model_profile=MODEL, inference_backend='torch'): + profile = trial_profile(model_profile, inference_backend) firmware = firmware or tools / 'root/usr/share/seabios/vgabios-stdvga.bin' return [tools / 'bin/qemu-system-x86_64', '-name', 'volparossa-opencode-inference', '-no-user-config', '-nodefaults', '-machine', 'q35,accel=kvm', '-cpu', 'host', '-smp', '2', '-m', str(profile['guest_memory_mib']), @@ -574,8 +654,8 @@ def qemu_command(tools, scratch, firmware=None, model_profile=MODEL): '-sandbox', 'on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny'] -def validate_bundle(path, model_profile=MODEL): - profile = trial_profile(model_profile) +def validate_bundle(path, model_profile=MODEL, inference_backend='torch'): + profile = trial_profile(model_profile, inference_backend) canonical(path) require(path.stat().st_size < 512 * 1024**2, 'bundle_bound') with tarfile.open(path) as archive: @@ -589,6 +669,7 @@ def validate_bundle(path, model_profile=MODEL): manifest_entry = archive.getmember('INPUTS.json') require(manifest_entry.size <= 2 * 1024**2, 'manifest_bound') manifest = json.load(archive.extractfile(manifest_entry)) + validate_backend_fields(manifest, profile) require(manifest['core_revision'] == profile['core_revision'] and manifest['model_profile'] == model_profile and manifest['code_contains_uncommitted_changes'] is True and manifest['code_git_head_proves_migration'] is False @@ -603,7 +684,8 @@ def validate_bundle(path, model_profile=MODEL): def execute(args): - profile = trial_profile(getattr(args, 'model_profile', MODEL)) + backend = getattr(args, 'inference_backend', 'torch') + profile = trial_profile(getattr(args, 'model_profile', MODEL), backend) os.umask(0o077) require(args.yes and os.getuid() > 0, 'explicit_unprivileged_execution') new_output(args.output) @@ -624,7 +706,7 @@ def execute(args): run([sys.executable, '-B', args.core / 'tests/integration/browser-native-tools.py', '--verify', '--output', canonical(args.tools)]) host_tools = None - manifest = validate_bundle(args.bundle, profile['model_profile']) + manifest = validate_bundle(args.bundle, profile['model_profile'], backend) # The host executes only this reviewed runner; guest code remains in KVM. require(manifest['files']['code/scripts/smoke_opencode_inference.py']['sha256'] == digest(Path(__file__)), 'runner_differs_from_captured_input') @@ -655,6 +737,7 @@ def execute(args): host_raw_route_dns_bytes_unchanged=None, host_observation_scope='proc_visible_routes_and_resolv_conf', host_firewall_modified=False, actual_model_execution_proven=False, confidential_remote_execution_proven=False) + receipt.update(backend_fields(profile)) if host_tools is not None: receipt['host_tools_profile'] = tools_profile receipt['host_tools_sha256'] = hashlib.sha256(json.dumps(host_tools, sort_keys=True).encode()).hexdigest() @@ -709,7 +792,7 @@ def interrupted(*_): receipt['host_available_bytes_at_launch'] = available qemu = qemu_command(args.tools, scratch, Path('/usr/share/seabios/vgabios-stdvga.bin') if host_tools is not None else None, - profile['model_profile']) + profile['model_profile'], backend) run(['systemd-run', '--user', '--quiet', '--unit=' + name, '--property=Type=exec', '--property=RemainAfterExit=yes', '--property=MemoryMax=' + str(profile['qemu_memory_bytes']), '--property=MemorySwapMax=0', @@ -749,10 +832,14 @@ def interrupted(*_): receipt['phase'] = 'guest-inference' print(json.dumps({'phase': receipt['phase'], 'model_profile': profile['model_profile']}), flush=True) executed = ssh('python3', '-B', str(INPUT / 'code/scripts/smoke_opencode_inference.py'), - 'guest', '--model-profile', profile['model_profile'], timeout=6600, check=False) + 'guest', '--model-profile', profile['model_profile'], + '--inference-backend', backend, timeout=6600, check=False) receipt['guest_exit_status'] = executed.returncode scp('vpci@127.0.0.1:/home/vpci/opencode-result.json', args.output / 'guest-result.json') result = load(args.output / 'guest-result.json', 65536) + validate_backend_fields(result, profile) + require(result.get('core_revision') == profile['core_revision'] + and result.get('model_profile') == profile['model_profile'], 'guest_source_profile') receipt['actual_model_execution_proven'] = bool(result.get('passed') and result.get('actual_model_provisioned')) require(executed.returncode == 0 and result.get('passed') is True, 'guest_trial_failed') receipt['phase'], status = 'complete', 0 @@ -816,6 +903,7 @@ def main(): execution.add_argument('--' + name, type=Path, required=True) for subparser in (packing, guest_parser, execution): subparser.add_argument('--model-profile', choices=MODEL_PROFILES, default=MODEL) + subparser.add_argument('--inference-backend', choices=INFERENCE_BACKENDS, default='torch') args = parser.parse_args() if args.mode == 'pack': pack(args) diff --git a/tests/test_opencode_ci.py b/tests/test_opencode_ci.py index ba82f81..17680ff 100644 --- a/tests/test_opencode_ci.py +++ b/tests/test_opencode_ci.py @@ -129,6 +129,36 @@ def test_default_guest_keeps_reviewed_task_and_acceptance_with_closed_diagnostic node = next(node for node in ast.parse(current).body if isinstance(node, ast.FunctionDef) and node.name == 'guest') source = ast.get_source_segment(current, node) + # Specialize only the explicit native opt-in to its unchanged torch + # default. Keep the original fingerprint; do not replace the golden. + native_blocks = [child for child in ast.walk(node) if isinstance(child, ast.If) + and ast.unparse(child.test) == "backend != 'torch'"] + self.assertEqual(len(native_blocks), 2) + for child in reversed(sorted(native_blocks, key=lambda item: item.lineno)): + lines = source.splitlines(keepends=True) + del lines[child.lineno - node.lineno:child.end_lineno - node.lineno + 1] + source = ''.join(lines) + changes = ( + (" backend = getattr(args, 'inference_backend', 'torch')\n", ''), + ("trial_profile(getattr(args, 'model_profile', MODEL), backend)", + "trial_profile(getattr(args, 'model_profile', MODEL))"), + ("staged_inputs(profile['model_profile'], backend)", "staged_inputs(profile['model_profile'])"), + (" report.update(backend_fields(profile))\n", ''), + (" provision_pins = (model.load_pins(profile['model_profile'], native_cpu_converter=True)\n" + " if backend != 'torch' else model.load_pins(profile['model_profile']))\n", + " provision_pins = model.load_pins(profile['model_profile'])\n"), + (",\n *(['--native-cpu-source', str(BASE / 'llama-source'), '--native-cpu-build', str(BASE / 'llama-build')]\n" + " if backend != 'torch' else [])", ''), + (",\n *(['--native-backend-root', str(BASE / 'ml/native-backend'), '--native-backend-sha256',\n" + " report['native_backend']['manifest_sha256']] if backend != 'torch' else [])", ''), + ("'core_memory' if name == CORE_UNIT else\n 'native_build_memory' if name == NATIVE_UNIT else 'task_memory'", + "'core_memory' if name == CORE_UNIT else 'task_memory'"), + ("(\n native_memory(profile['core_memory_bytes']) if name == NATIVE_UNIT else memory(name))", "memory(name)"), + (",\n native_cpu=backend != 'torch'", ''), + ) + for added, original in changes: + self.assertIn(added, source) + source = source.replace(added, original) selection = " profile = trial_profile(getattr(args, 'model_profile', MODEL))\n" guard = " require(report['task']['model_profile'] == profile['model_profile'], 'task_model_mismatch')\n" self.assertIn(selection, source) @@ -180,6 +210,7 @@ def test_workflow_has_one_manual_trial_and_closed_export_only(self): 'steps.selected_core.outputs.core_revision', 'persist-credentials: false', 'default: qwen3-0.6b-v1', 'qwen3-4b-instruct-2507-v1', '--model-profile "$MODEL_PROFILE"', '--host-tools-profile github-ubuntu-24.04', + 'default: torch', '--inference-backend "$INFERENCE_BACKEND"', 'llama_cpp_bf16_v1', 'env -i PATH=/usr/bin:/bin', 'build/ci-public-receipts/*.json'): self.assertIn(required, source) for forbidden in ('pull_request:', '\n push:', 'actions/cache', 'upload-artifact@main', diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py new file mode 100644 index 0000000..7aa3cd9 --- /dev/null +++ b/tests/test_opencode_native_cpu.py @@ -0,0 +1,296 @@ +# SPDX-License-Identifier: GPL-3.0-only +"""Inert native fixture bindings; no source fetch, build, library or model load.""" +import hashlib +import importlib.util +import inspect +import io +import json +import os +from pathlib import Path +import subprocess +import tarfile +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] + + +def load(name): + spec = importlib.util.spec_from_file_location(name, ROOT / 'scripts' / (name + '.py')) + value = importlib.util.module_from_spec(spec) + spec.loader.exec_module(value) + return value + + +NATIVE = load('opencode_native_cpu') +TRIAL = load('smoke_opencode_inference') +CI = load('opencode_ci') +KIND = 'llama_cpp_bf16_v1' + + +class Contracts(unittest.TestCase): + def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): + self.assertEqual(TRIAL.NATIVE_CORE, 'f7e2c3b9c2abd710682a2ec51d4b5dfbc46b9d8f') + original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) + self.assertNotIn('inference_backend', original) + with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): + TRIAL.trial_profile(TRIAL.LARGE_MODEL, KIND) + for model in (TRIAL.MODEL,): + with self.assertRaisesRegex(ValueError, 'native_backend_requires_qwen4b'): + TRIAL.trial_profile(model, KIND) + for backend in (None, '', 'llama_cpp', 'torch;id'): + with self.assertRaisesRegex(ValueError, 'unknown_inference_backend'): + TRIAL.trial_profile(TRIAL.LARGE_MODEL, backend) + # Synthetic pin tests selection only; no invented production pin is set. + with patch.object(TRIAL, 'NATIVE_CORE', 'a' * 40): + native = TRIAL.trial_profile(TRIAL.LARGE_MODEL, KIND) + self.assertEqual({k: v for k, v in native.items() if k not in + ('core_revision', 'inference_backend', 'native_source_commit')}, + {k: v for k, v in original.items() if k != 'core_revision'}) + self.assertEqual(native['native_source_commit'], NATIVE.SOURCE) + self.assertEqual(TRIAL.trial_profile(TRIAL.LARGE_MODEL), original) + old = TRIAL.qemu_command(Path('/tools'), Path('/scratch'), model_profile=TRIAL.LARGE_MODEL) + new = TRIAL.qemu_command(Path('/tools'), Path('/scratch'), model_profile=TRIAL.LARGE_MODEL, + inference_backend=KIND) + self.assertEqual(old, new) + + def test_backend_metadata_cannot_be_missing_partial_or_implicitly_added(self): + with patch.object(TRIAL, 'NATIVE_CORE', 'a' * 40): + profile = TRIAL.trial_profile(TRIAL.LARGE_MODEL, KIND) + exact = {'inference_backend': KIND, 'native_source_commit': NATIVE.SOURCE} + TRIAL.validate_backend_fields(exact, profile) + self.assertEqual(CI.backend_fields(profile), exact) + for value in ({}, {'inference_backend': KIND}, {'native_source_commit': NATIVE.SOURCE}, + dict(exact, inference_backend='torch'), dict(exact, native_source_commit='0' * 40)): + with self.assertRaisesRegex(ValueError, 'input_backend'): + TRIAL.validate_backend_fields(value, profile) + for value in (exact, {'inference_backend': None}): + with self.assertRaisesRegex(ValueError, 'input_backend'): + TRIAL.validate_backend_fields(value, TRIAL.trial_profile()) + + def test_real_bundle_and_staging_parsers_reject_native_source_substitution(self): + payload = b'inert input bytes, no runtime or model' + row = dict(bytes=len(payload), sha256=hashlib.sha256(payload).hexdigest(), mode=0o600) + exact = dict(core_revision='a' * 40, model_profile=TRIAL.LARGE_MODEL, + inference_backend=KIND, native_source_commit=NATIVE.SOURCE, + code_contains_uncommitted_changes=True, code_git_head_proves_migration=False, + files={'code/example.cjs': row}) + with tempfile.TemporaryDirectory() as temp, patch.object(TRIAL, 'NATIVE_CORE', 'a' * 40): + root = Path(temp) + (root / 'code').mkdir() + (root / 'code/example.cjs').write_bytes(payload) + (root / 'code/example.cjs').chmod(0o600) + for value, accepted in ((exact, True), (dict(exact, core_revision='b' * 40), False), + (dict(exact, model_profile=TRIAL.MODEL), False), + (dict(exact, inference_backend='torch'), False), + (dict(exact, native_source_commit='b' * 40), False), + ({key: val for key, val in exact.items() + if key != 'native_source_commit'}, False)): + raw = json.dumps(value).encode() + path = root / 'inputs.tar.gz' + with tarfile.open(path, 'w:gz') as archive: + for name, content in (('code/example.cjs', payload), ('INPUTS.json', raw)): + entry = tarfile.TarInfo(name) + entry.mode, entry.size = 0o600, len(content) + archive.addfile(entry, io.BytesIO(content)) + (root / 'INPUTS.json').write_bytes(raw) + with self.subTest(value=value), patch.object(TRIAL, 'INPUT', root): + for parse in (lambda: TRIAL.validate_bundle(path, TRIAL.LARGE_MODEL, KIND), + lambda: TRIAL.staged_inputs(TRIAL.LARGE_MODEL, KIND)): + if accepted: + self.assertEqual(parse(), exact) + else: + with self.assertRaises(ValueError): + parse() + + def test_ci_capture_binds_same_backend_and_rejects_mixed_source_receipts(self): + with tempfile.TemporaryDirectory() as temp, patch.object(TRIAL, 'NATIVE_CORE', 'a' * 40), \ + patch.object(CI, 'BUILD', Path(temp)), patch.object(CI, 'guard'), \ + patch.object(CI, 'module', return_value=TRIAL), \ + patch.dict(os.environ, {'GITHUB_SHA': 'c' * 40}): + root = Path(temp) + manifest = dict(code_base_revision='c' * 40, core_revision='a' * 40, + model_profile=TRIAL.LARGE_MODEL, inference_backend=KIND, + native_source_commit=NATIVE.SOURCE) + source = dict(manifest, code_revision='c' * 40) + (root / 'ci-source.json').write_text(json.dumps(source)) + (root / 'ci-inputs.tar.gz').write_bytes(b'inert bundle, parser checked separately') + with patch.object(TRIAL, 'validate_bundle', return_value=manifest) as parser: + CI.capture(TRIAL.LARGE_MODEL, KIND) + parser.assert_called_once_with(root / 'ci-inputs.tar.gz', TRIAL.LARGE_MODEL, KIND) + captured = json.loads((root / 'ci-inputs.json').read_text()) + self.assertEqual(captured['inference_backend'], KIND) + self.assertEqual(captured['native_source_commit'], NATIVE.SOURCE) + (root / 'ci-source.json').write_text(json.dumps(dict(source, inference_backend='torch'))) + with self.assertRaisesRegex(ValueError, 'input_backend'): + CI.capture(TRIAL.LARGE_MODEL, KIND) + + def test_native_missing_pin_stops_before_memory_tools_or_vm(self): + with patch.object(TRIAL, 'NATIVE_CORE', None), patch.object(TRIAL, 'run') as process, \ + patch.object(TRIAL, 'available_memory') as memory: + with self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): + TRIAL.execute(SimpleNamespace(model_profile=TRIAL.LARGE_MODEL, inference_backend=KIND)) + process.assert_not_called() + memory.assert_not_called() + + def test_only_native_build_unit_can_receive_public_source_network(self): + with tempfile.TemporaryDirectory() as temp, patch.object(TRIAL, 'BASE', Path(temp)), \ + patch.object(TRIAL, 'run') as process: + for unit in (TRIAL.CORE_UNIT, TRIAL.TASK_UNIT, 'untrusted.service'): + with self.assertRaisesRegex(ValueError, 'native_build_network_scope'): + TRIAL.start(unit, ['inert'], 11 * TRIAL.GIB, 1800, public_source_build=True) + process.assert_not_called() + TRIAL.start(TRIAL.NATIVE_UNIT, ['inert'], 11 * TRIAL.GIB, 1800, public_source_build=True) + args = process.call_args.args[0] + for field in ('--property=PrivateNetwork=no', '--property=MemoryMax=' + str(11 * TRIAL.GIB), + '--property=MemorySwapMax=0', '--property=RuntimeMaxSec=1800', + '--property=KillMode=control-group', '--property=NoNewPrivileges=yes'): + self.assertIn(field, args) + TRIAL.start(TRIAL.CORE_UNIT, ['inert'], 11 * TRIAL.GIB, 2700) + self.assertIn('--property=PrivateNetwork=yes', process.call_args.args[0]) + with patch.object(TRIAL.subprocess, 'run', return_value=subprocess.CompletedProcess([], 0)): + TRIAL.unit(TRIAL.NATIVE_UNIT, 'stop') + self.assertEqual(TRIAL.cgroup(TRIAL.NATIVE_UNIT).name, TRIAL.NATIVE_UNIT) + + def test_native_helper_refuses_host_before_acquisition_or_build(self): + with patch.object(NATIVE.os, 'getuid', return_value=0), patch.object(NATIVE.subprocess, 'run') as process: + with self.assertRaisesRegex(ValueError, 'disposable_guest_required'): + NATIVE.guest_build() + process.assert_not_called() + + def test_acquisition_and_build_use_exact_source_in_guest_not_prebuilt_payload(self): + source = inspect.getsource(NATIVE.guest_build) + for text in ("'fetch', '--depth=1', '--no-tags', 'origin', SOURCE", 'builder.verify_source(source)', + "'--source', str(source), '--output', str(output), '--execute'", + "'GIT_CONFIG_GLOBAL': '/dev/null'", 'https://github.com/ggml-org/llama.cpp.git'): + self.assertIn(text, source if text.startswith(('builder', "'fetch'", "'--source'")) + else (ROOT / 'scripts/opencode_native_cpu.py').read_text()) + self.assertNotIn('ctypes.CDLL', source) + self.assertNotIn('curl', source) + + def test_guest_keeps_original_task_and_inference_budgets_and_joins_native_unit(self): + source = inspect.getsource(TRIAL.guest) + for text in ("model.load_pins(profile['model_profile'], native_cpu_converter=True)", + 'model.retained_pin_files(pins)', "'--native-cpu-source'", "'--native-cpu-build'", + "'--native-backend-root'", "'--native-backend-sha256'", + "report['native_backend']['manifest_sha256']", 'provision.wait(timeout=1850)', + "'--threads', '2', '--max-seconds', '600'", '768 * 1024**2, 2550', + 'time.monotonic() + 2565', 'created.append(NATIVE_UNIT)', 'for name in reversed(created)', + "report['units_empty'] = all(empty(name) for name in created)", + "if report['units_empty'] and report['provision_group_joined']:"): + self.assertIn(text, source) + task_driver = (ROOT / 'scripts/smoke_opencode_inference.cjs').read_bytes() + # Exact unchanged source at dd7a709, usable also in a shallow checkout; + # not regenerated tests, injected answers or a claim of model quality. + self.assertEqual(hashlib.sha256(task_driver).hexdigest(), + '702f0122475ca2e7da6b3f98b446071dcd3baf06af097168cffaa43631f1e484') + + def test_native_memory_requires_actual_zero_swap_and_oom_not_just_limits(self): + with tempfile.TemporaryDirectory() as temp, patch.object(TRIAL, 'cgroup', return_value=Path(temp)): + root, limit = Path(temp), 11 * TRIAL.GIB + original = {'memory.max': str(limit), 'memory.swap.max': '0', 'memory.peak': '123456', + 'memory.swap.current': '0', 'memory.events': 'low 0\nhigh 0\nmax 0\noom 0\noom_kill 0\n'} + def write(values): + for name, text in values.items(): + (root / name).write_text(text) + write(original) + observed = TRIAL.native_memory(limit) + self.assertEqual(observed['swap_current'], 0) + self.assertEqual(observed['oom'], 0) + for mutation in ({'memory.swap.current': '1'}, {'memory.swap.max': '1'}, + {'memory.max': str(limit + 1)}, {'memory.peak': str(limit + 1)}, + {'memory.events': 'oom 1\noom_kill 0\n'}, + {'memory.events': 'oom 0\noom_kill 1\n'}): + write(dict(original, **mutation)) + with self.assertRaisesRegex(ValueError, 'native_build_resource_violation'): + TRIAL.native_memory(limit) + write(original) + (root / 'memory.swap.current').unlink() + with self.assertRaises(OSError): + TRIAL.native_memory(limit) + + def test_native_conversion_diagnostic_is_exact_opt_in_and_no_raw_export(self): + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / 'provision.log' + path.write_bytes(b'NATIVE_CONVERSION_FAILED\nProvisioning refused: Command CANARY returned non-zero exit status 1.\n') + path.chmod(0o600) + legacy = TRIAL.closed_provision(path, None, 'process', 1) + native = TRIAL.closed_provision(path, None, 'process', 1, native_cpu=True) + self.assertEqual(set(legacy), set(native)) + self.assertEqual(legacy['failure_class'], 'runtime_subprocess') + self.assertEqual(native['failure_class'], 'native_conversion') + self.assertNotIn('CANARY', json.dumps(native)) + for raw in (b'NATIVE_CONVERSION_FAILED CANARY\n', b'prefix NATIVE_CONVERSION_FAILED\n', + b'NATIVE_CONVERSION_FAILED\n' + b'x' * 131073): + path.write_bytes(raw) + self.assertNotEqual(TRIAL.closed_provision(path, None, 'process', 1, native_cpu=True) + ['failure_class'], 'native_conversion') + + def test_native_unit_diagnostics_are_closed_and_do_not_invent_log_inspection(self): + value = TRIAL.closed_native_unit({'ActiveState': 'failed', 'Result': 'timeout', + 'ExecMainStatus': '9', 'private': 'CANARY'}) + self.assertEqual(value, {'active': 'failed', 'result': 'timeout', 'status': 9}) + self.assertNotIn('CANARY', json.dumps(value)) + self.assertNotIn('stderr', json.dumps(value)) + + def bundle_fixture(self, directory): + base, core = Path(directory) / 'base', Path(directory) / 'core' + build_root, backend = base / 'llama-build', base / 'ml/native-backend' + wrappers = core / 'workers/volparossa-ml/native-cpu' + for path in (build_root, backend, wrappers): + path.mkdir(mode=0o700, parents=True) + def artifact(path, content): + path.write_bytes(content) + return {'bytes': len(content), 'sha256': hashlib.sha256(content).hexdigest()} + library = artifact(build_root / 'libvolparossa_llama_cpu.so', b'inert library bytes, never loaded') + (backend / 'libvolparossa_llama_cpu.so').write_bytes((build_root / 'libvolparossa_llama_cpu.so').read_bytes()) + wrapper = {name: artifact(wrappers / name, b'inert source ' + name.encode()) + for name in ('CMakeLists.txt', 'adapter.h', 'adapter.cpp')} + raw_build = dict(version=1, kind=KIND, source_commit=NATIVE.SOURCE, source_tree='b' * 40, + abi_version=1, jobs=2, cpu='avx2_fma_f16c', models_loaded=False, downloads=False, + install=False, sanitizers=False, provisionable=True, quantization=False, + library={'path': 'libvolparossa_llama_cpu.so', **library}, wrapper_sources=wrapper, + dynamic_dependencies=['libc.so.6']) + for root in (build_root, backend): + (root / 'build.json').write_text(json.dumps(raw_build)) + build_hash = hashlib.sha256((build_root / 'build.json').read_bytes()).hexdigest() + gguf = artifact(backend / 'model.gguf', b'inert gguf bytes, never loaded') + manifest = dict(model_profile=NATIVE.MODEL, build_manifest_sha256=build_hash, + library=raw_build['library'], gguf={'path': 'model.gguf', **gguf}, source_weights_sha256='c' * 64) + (backend / 'backend.json').write_text(json.dumps(manifest)) + report = dict(native_cpu_converter={'implementation': KIND, 'dependencies': {'sentencepiece': '0.2.1'}}, + native_backend={'kind': KIND, 'root': str(backend), + 'backend_sha256': hashlib.sha256((backend / 'backend.json').read_bytes()).hexdigest()}) + # Core parsers/builders have separate tests; these doubles exercise only + # this fixture consumer's artifact/provenance checks, not model evidence. + builder = SimpleNamespace(SOURCE=NATIVE.SOURCE, ORIGIN=NATIVE.ORIGIN, KIND=KIND, + ALLOWED_NEEDED={'libc.so.6'}, verify_source=lambda _path: 'b' * 40) + parser = SimpleNamespace(validate_manifest=lambda _value, _require: None) + return base, core, report, builder, parser + + def test_exact_build_and_converted_manifest_bind_actual_inert_bytes(self): + with tempfile.TemporaryDirectory() as temp: + base, core, report, builder, parser = self.bundle_fixture(temp) + with patch.object(NATIVE, 'module', return_value=builder): + build = NATIVE.validate_build(core, base) + with patch.object(NATIVE, 'module', return_value=parser): + result = NATIVE.validate_provision(core, base, report, build) + self.assertEqual(result['manifest_sha256'], report['native_backend']['backend_sha256']) + self.assertNotIn(str(base), json.dumps(result)) + for mutation in ({'native_cpu_converter': {}}, + {'native_backend': dict(report['native_backend'], root='/untrusted')}, + {'native_backend': dict(report['native_backend'], backend_sha256='0' * 64)}): + with self.assertRaises(ValueError): + NATIVE.validate_provision(core, base, dict(report, **mutation), build) + (base / 'ml/native-backend/model.gguf').write_bytes(b'changed') + with self.assertRaisesRegex(ValueError, 'native_artifact_binding'): + NATIVE.validate_provision(core, base, report, build) + (core / 'workers/volparossa-ml/native-cpu/adapter.cpp').write_bytes(b'changed') + with patch.object(NATIVE, 'module', return_value=builder), self.assertRaisesRegex(ValueError, 'native_wrapper_binding'): + NATIVE.validate_build(core, base) + + +if __name__ == '__main__': + unittest.main() From e838d5d23b89e7008f0e126a38c5a53984a36b8a Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:23:46 +0200 Subject: [PATCH 06/39] test: carry native backend through exact build source gate --- .github/workflows/source-checks.yml | 1 - docs/OPENCODE.md | 18 +++++---- scripts/opencode_ci_build.sh | 2 +- tests/test_opencode_native_cpu.py | 62 +++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/.github/workflows/source-checks.yml b/.github/workflows/source-checks.yml index c2750c0..51dbfad 100644 --- a/.github/workflows/source-checks.yml +++ b/.github/workflows/source-checks.yml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - branches: [main] permissions: contents: read diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index d56f388..81c321e 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -703,11 +703,11 @@ receipts are exported. The existing pinned Debian workspace-tool path is unchanged. Focused offline contracts and shell/syntax checks pass; hosted admission has been exercised, but actual coding completion remains unproved. -For pre-merge testing, the identical manual workflow file must first exist on -the default branch. Dispatch it on `feature/opencode-integration`, supplying -that exact reviewed commit as `expected_code_sha`; dispatching an unprepared -main branch cannot pass the source guard and must not install tools or launch -a guest. There is no automatic inference run on push or pull request. +For pre-merge testing, the manual workflow must be registered on the default +branch. Dispatch the reviewed candidate branch and supply its exact commit as +`expected_code_sha`; the [selected ref identifies the workflow version](https://cli.github.com/manual/gh_workflow_run). +An unprepared branch cannot pass the source guard and must not install tools or +launch a guest. There is no automatic inference run on push or pull request. ### Explicit native CPU candidate @@ -744,9 +744,13 @@ request budget, coding task and independent tests are unchanged. This is not quantization, a smaller model, confidential remote execution or a new app-level peer scheduler. -Forty-three targeted Python fixture checks and forty-seven Node consumer/session +Forty-five targeted Python fixture checks and forty-seven Node consumer/session checks pass. They cover exact source/backend/artifact bindings, negative cases, -unchanged defaults, measured resource limits and cleanup contracts. No actual +unchanged defaults, measured resource limits and cleanup contracts. The actual +build-shell selector now forwards the explicit backend choice too; an inert +shell-prefix regression reproduces the previous mismatch before any profile, +privileged operation or build. Source-only CI also covers stacked pull requests, +with unchanged checks and permissions. No actual conversion, native model inference or successful coding loop has yet been proved. The core's retained full upstream sanitizer failure on a disallowed quantized path also remains explicit; separate BF16/F32 checks do not erase that failure. diff --git a/scripts/opencode_ci_build.sh b/scripts/opencode_ci_build.sh index 5ba0fa0..8b770d7 100644 --- a/scripts/opencode_ci_build.sh +++ b/scripts/opencode_ci_build.sh @@ -5,7 +5,7 @@ set -euo pipefail test "$#" -eq 0 python3 -B scripts/opencode_ci.py guard core="$PWD/build/ci-core" -selected_core=$(python3 -B scripts/opencode_ci.py select --model-profile "${MODEL_PROFILE:-qwen3-0.6b-v1}") +selected_core=$(python3 -B scripts/opencode_ci.py select --model-profile "${MODEL_PROFILE:-qwen3-0.6b-v1}" --inference-backend "${INFERENCE_BACKEND:-torch}") test "$(git -C "$core" rev-parse HEAD)" = "${selected_core#core_revision=}" profile="$core/tests/integration/native-coding-bwrap.apparmor" test "$(sha256sum "$profile" | cut -d ' ' -f 1)" = 3f3fefdfc6fe46e882af9b803ddcddd6691083e434b26f5fb244ceddf05b6794 diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 7aa3cd9..3335882 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -160,6 +160,68 @@ def test_native_helper_refuses_host_before_acquisition_or_build(self): NATIVE.guest_build() process.assert_not_called() + def test_actual_build_shell_prefix_forwards_backend_before_any_profile_or_sudo(self): + script = (ROOT / 'scripts/opencode_ci_build.sh').read_text() + prefix, separator, _ = script.partition('profile="$core/tests/integration/native-coding-bwrap.apparmor"') + self.assertTrue(separator) + addition = ' --inference-backend "${INFERENCE_BACKEND:-torch}"' + self.assertEqual(prefix.count(addition), 1) + # Execute the real shell prefix and the real CLI selector. Only its + # hosted-machine guard is replaced; no build, profile, sudo or VM runs. + bridge = '''python3() { + command /usr/bin/python3 -B -c ' +import importlib.util, sys +from pathlib import Path +assert sys.argv[1] == "-B" +path = Path(sys.argv[2]).resolve() +spec = importlib.util.spec_from_file_location("inert_ci_prefix", path) +value = importlib.util.module_from_spec(spec) +spec.loader.exec_module(value) +value.guard = lambda: None +sys.argv = [str(path), *sys.argv[3:]] +value.main() +' "$@" +} +git() { + test "$#" = 4 && test "$1" = -C && test "$3" = rev-parse && test "$4" = HEAD || return 99 + printf '%s\\n' "$TEST_EXPECTED_CORE" +} +sudo() { return 99; } +''' + def check(source, model=None, backend=None, core=TRIAL.CORE): + env = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'TEST_EXPECTED_CORE': core} + if model is not None: + env['MODEL_PROFILE'] = model + if backend is not None: + env['INFERENCE_BACKEND'] = backend + return subprocess.run(['/bin/bash', '-c', bridge + source + '\nprintf "PREFIX_ACCEPTED\\n"\n'], + cwd=ROOT, env=env, capture_output=True, text=True, timeout=10) + for model, backend, core in ((None, None, TRIAL.CORE), + (TRIAL.LARGE_MODEL, 'torch', TRIAL.LARGE_CORE), + (TRIAL.LARGE_MODEL, KIND, TRIAL.NATIVE_CORE)): + result = check(prefix, model, backend, core) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, 'PREFIX_ACCEPTED\n') + # The original prefix genuinely fails the native checkout, then the + # corrected same prefix passes above; failures never reach profile work. + for source, model, backend, core in ( + (prefix.replace(addition, ''), TRIAL.LARGE_MODEL, KIND, TRIAL.NATIVE_CORE), + (prefix, TRIAL.LARGE_MODEL, KIND, TRIAL.LARGE_CORE), + (prefix, TRIAL.MODEL, KIND, TRIAL.NATIVE_CORE), + (prefix, TRIAL.LARGE_MODEL, 'unknown', TRIAL.NATIVE_CORE)): + result = check(source, model, backend, core) + self.assertNotEqual(result.returncode, 0) + self.assertNotIn('PREFIX_ACCEPTED', result.stdout) + + def test_stacked_source_workflow_keeps_original_jobs_and_only_removes_pr_filter(self): + source = (ROOT / '.github/workflows/source-checks.yml').read_text() + self.assertIn(' push:\n branches: [main]\n pull_request:\n', source) + self.assertEqual(source.count('branches: [main]'), 1) + self.assertNotIn('feature/', source) + self.assertIn('permissions:\n contents: read\n', source) + self.assertIn('timeout-minutes: 5', source) + self.assertIn("python3 -B -m unittest discover -s tests -p 'test_*opencode*.py'", source) + def test_acquisition_and_build_use_exact_source_in_guest_not_prebuilt_payload(self): source = inspect.getsource(NATIVE.guest_build) for text in ("'fetch', '--depth=1', '--no-tags', 'origin', SOURCE", 'builder.verify_source(source)', From 77fc08a88237ca8dd74d403222dd0e1548ffb46c Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:50:17 +0200 Subject: [PATCH 07/39] fix(ci): pin native CPU trial to CLI group correction --- docs/OPENCODE.md | 12 +++++++++++- scripts/smoke_opencode_inference.py | 4 ++-- tests/test_opencode_native_cpu.py | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 81c321e..b2bf16c 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -723,11 +723,21 @@ stall's cause. Original artifact ZIP SHA-256: An explicitly selected `--inference-backend llama_cpp_bf16_v1` now prepares a different CPU executor for that same original 4B task. It pins core -`f7e2c3b9c2abd710682a2ec51d4b5dfbc46b9d8f` and llama.cpp source +`7308371b20ced0504662178beb0e46586cfc9d2d` and llama.cpp source `7fe450e19305b828c199d602c23a8337aaa1f03b`. The default `torch` backend and both existing model profiles retain their previous core pins. Native selection is refused for the smaller profile; there is no silent backend fallback. +The previous candidate core `f7e2c3b9c2abd710682a2ec51d4b5dfbc46b9d8f` +failed [source Quality run 37235233676](https://github.com/VOLPAROSSA/volparossa/actions/runs/37235233676) +in four full command-tree tests because two flattened Clap argument groups +shared the name `Options`. No native inference trial was dispatched. The new +pin gives the native options a unique group ID; targeted command-tree and +absent/complete/incomplete flag-pair regressions pass. That fixes the reproduced +CLI construction error, not the earlier model-forward stall. The original +failed source run remains failure evidence; the new source still requires its +own passing CI before any trial. + Select the native backend consistently for source selection, packing and execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow exposes the same explicit choice. It must be registered and dispatched against diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 4a44366..264c2e7 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -35,8 +35,8 @@ LARGE_CORE = '6a517b576baa17e7329661ee0476d1848081d114' MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') -# Separately reviewed native converter dependency and process-group cleanup. -NATIVE_CORE = 'f7e2c3b9c2abd710682a2ec51d4b5dfbc46b9d8f' +# Native converter/process cleanup plus the reviewed unique CLI argument group. +NATIVE_CORE = '7308371b20ced0504662178beb0e46586cfc9d2d' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 3335882..fd795f3 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -32,7 +32,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, 'f7e2c3b9c2abd710682a2ec51d4b5dfbc46b9d8f') + self.assertEqual(TRIAL.NATIVE_CORE, '7308371b20ced0504662178beb0e46586cfc9d2d') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 01c5c591b16ed34226341e04fe673a35a7a169de Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:17:58 +0200 Subject: [PATCH 08/39] Observe native build resources before helper exit --- docs/OPENCODE.md | 29 ++- scripts/opencode_native_cpu.py | 81 +++++++- scripts/smoke_opencode_inference.py | 125 +++++++++-- tests/test_opencode_native_cpu.py | 311 +++++++++++++++++++++++++++- 4 files changed, 522 insertions(+), 24 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index b2bf16c..74ca517 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -738,6 +738,30 @@ CLI construction error, not the earlier model-forward stall. The original failed source run remains failure evidence; the new source still requires its own passing CI before any trial. +That core's source checks subsequently passed. The first explicitly native +[trial 37239050033](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37239050033) +on Code `77fc08a88237ca8dd74d403222dd0e1548ffb46c` remains **failed** at +`native-source-build`. The original report records an active build unit with +result `success` and status `0`, but no completed resource observation or retained +build-manifest binding. No model was provisioned and no coding task ran. Owned +guest units/private files and outer QEMU/scratch cleanup passed; the recorded +outer routes/DNS comparison was unchanged. Original artifact ZIP SHA-256: +`ed5f8c699806fc1f4f7438ca2ffc3ded2e26a8cd7d95e445204cd9c815ac2e07`. + +The fixture had read cgroup resource files only after helper exit. systemd may +[prune that empty cgroup even while keeping the unit active/exited](https://github.com/systemd/systemd/blob/v257/src/core/service.c#L1203-L1206). +An inert reproduction gives the same missing-resource report shape. The original +exception and substate were not retained, so this is not a uniquely established +retrospective cause. The follow-up uses a bounded, source-bound local socket +ready/release handshake: after joining the compiler and checking provenance, +the helper stays alive while its parent checks the exact unit, sole remaining +helper PID and actual cgroup counters. Only then may it exit, still requiring +status `0` and an empty unit. The same absolute 1,800-second build deadline +includes the handshake. Missing/mismatched/late messages, unavailable counters +and failed exits remain failures. Closed substate, stage and exception-category +diagnostics expose no raw logs or private paths. This correction is not yet +verified by another actual native trial. + Select the native backend consistently for source selection, packing and execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow exposes the same explicit choice. It must be registered and dispatched against @@ -754,8 +778,9 @@ request budget, coding task and independent tests are unchanged. This is not quantization, a smaller model, confidential remote execution or a new app-level peer scheduler. -Forty-five targeted Python fixture checks and forty-seven Node consumer/session -checks pass. They cover exact source/backend/artifact bindings, negative cases, +Fifty-seven targeted Python fixture checks pass; the unchanged consumer/session +code retains its earlier forty-seven passing Node checks. These checks cover +exact source/backend/artifact bindings, negative cases, unchanged defaults, measured resource limits and cleanup contracts. The actual build-shell selector now forwards the explicit backend choice too; an inert shell-prefix regression reproduces the previous mismatch before any profile, diff --git a/scripts/opencode_native_cpu.py b/scripts/opencode_native_cpu.py index 867905d..7f5c78e 100644 --- a/scripts/opencode_native_cpu.py +++ b/scripts/opencode_native_cpu.py @@ -15,7 +15,9 @@ import re import socket import stat +import struct import subprocess +import time SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ORIGIN = 'https://github.com/ggml-org/llama.cpp.git' @@ -23,6 +25,8 @@ MODEL = 'qwen3-4b-instruct-2507-v1' BASE = Path('/home/vpci/opencode-trial') CORE = Path('/home/vpci/source') +CONTROL = 'native-build-control.sock' +FRAME_MAX = 1024 ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1', 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': '/dev/null', 'GIT_TERMINAL_PROMPT': '0'} @@ -39,6 +43,64 @@ def module(path, name): return value +def remaining(deadline): + require(type(deadline) is int and 0 < deadline < 2 ** 63, 'native_deadline') + seconds = (deadline - time.monotonic_ns()) / 1_000_000_000 + require(0 < seconds <= 1800, 'native_deadline') + return seconds + + +def binding(kind, nonce, manifest): + require(kind in ('ready', 'release') and type(nonce) is str + and re.fullmatch('[0-9a-f]{64}', nonce) and type(manifest) is str + and re.fullmatch('[0-9a-f]{64}', manifest), 'native_handshake_binding') + return (json.dumps(dict(version=1, kind='native_build_' + kind, nonce=nonce, + source_commit=SOURCE, build_manifest_sha256=manifest), + sort_keys=True, separators=(',', ':')) + '\n').encode() + + +def send_frame(channel, raw, deadline): + require(type(raw) is bytes and 0 < len(raw) <= FRAME_MAX, 'native_handshake_frame') + channel.settimeout(remaining(deadline)) + channel.sendall(raw) + channel.shutdown(socket.SHUT_WR) + + +def receive_frame(channel, deadline): + raw = bytearray() + while True: + channel.settimeout(remaining(deadline)) + part = channel.recv(FRAME_MAX + 1 - len(raw)) + if not part: + require(0 < len(raw) <= FRAME_MAX, 'native_handshake_frame') + return bytes(raw) + raw.extend(part) + require(len(raw) <= FRAME_MAX, 'native_handshake_frame') + + +def peer(channel, expected_pid): + observed = struct.unpack('3i', channel.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)) + require(type(expected_pid) is int and expected_pid > 1 + and observed == (expected_pid, os.getuid(), os.getgid()), 'native_handshake_peer') + + +def await_release(base, build, nonce, deadline, parent_pid): + # The compiler has been joined and its provenance verified before readiness. + # Keep this sole remaining helper alive while its parent reads real cgroup counters. + path = base / CONTROL + info = path.lstat() + require(stat.S_ISSOCK(info.st_mode) and info.st_uid == os.getuid() + and stat.S_IMODE(info.st_mode) == 0o600, 'native_handshake_socket') + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as channel: + channel.settimeout(remaining(deadline)) + channel.connect(str(path)) + peer(channel, parent_pid) + send_frame(channel, binding('ready', nonce, build['build_manifest_sha256']), deadline) + require(receive_frame(channel, deadline) == binding('release', nonce, build['build_manifest_sha256']), + 'native_handshake_binding') + remaining(deadline) + + def file_identity(path, maximum): with os.fdopen(os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK), 'rb') as stream: info = os.fstat(stream.fileno()) @@ -107,12 +169,13 @@ def validate_provision(core, base, report, build): source_weights_sha256=value['source_weights_sha256']) -def guest_build(): +def guest_build(deadline=None): require(os.getuid() > 0 and pwd.getpwuid(os.getuid()).pw_name == 'vpci' and socket.gethostname() == 'volparossa-alpha' and subprocess.run(['systemd-detect-virt', '--vm'], check=True, capture_output=True, text=True, timeout=10).stdout.strip() == 'kvm' and 'VERSION_ID="13"' in Path('/etc/os-release').read_text(), 'disposable_guest_required') + remaining(deadline) os.umask(0o077) source, output = BASE / 'llama-source', BASE / 'llama-build' require(not source.exists() and not source.is_symlink() and not output.exists() and not output.is_symlink(), @@ -122,7 +185,7 @@ def guest_build(): for operation in (['init', '--quiet'], ['remote', 'add', 'origin', ORIGIN], ['fetch', '--depth=1', '--no-tags', 'origin', SOURCE], ['checkout', '--detach', '--quiet', 'FETCH_HEAD']): subprocess.run([*git, *operation], cwd=source, env=ENV, check=True, stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=300) + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=min(300, remaining(deadline))) builder = module(CORE / 'workers/volparossa-ml/build_llama_cpu.py', 'trial_native_build') require(builder.SOURCE == SOURCE and builder.ORIGIN == ORIGIN, 'native_builder_pin') builder.verify_source(source) @@ -131,16 +194,22 @@ def guest_build(): # compiler cleanup. No source acquisition or compilation runs on the host. subprocess.run([os.sys.executable, '-B', str(CORE / 'workers/volparossa-ml/build_llama_cpu.py'), '--source', str(source), '--output', str(output), '--execute'], - env=ENV, check=True, timeout=1800) - validate_build(CORE, BASE) + env=ENV, check=True, timeout=remaining(deadline)) + return validate_build(CORE, BASE) if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--execute', action='store_true', required=True) - parser.parse_args() + parser.add_argument('--deadline-monotonic-ns', type=int, required=True) + parser.add_argument('--handshake-nonce', required=True) + parser.add_argument('--parent-pid', type=int, required=True) + args = parser.parse_args() try: - guest_build() + binding('ready', args.handshake_nonce, '0' * 64) + require(args.parent_pid > 1, 'native_handshake_peer') + build = guest_build(args.deadline_monotonic_ns) + await_release(BASE, build, args.handshake_nonce, args.deadline_monotonic_ns, args.parent_pid) except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError): print('NATIVE_GUEST_SOURCE_BUILD_FAILED') raise SystemExit(1) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 264c2e7..c627b3a 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -326,11 +326,29 @@ def closed_native_unit(state): code = state.get('ExecMainStatus', '') return dict(active=state.get('ActiveState') if state.get('ActiveState') in ('active', 'inactive', 'failed', 'activating', 'deactivating') else 'unknown', + substate=state.get('SubState') if state.get('SubState') in + ('running', 'exited', 'failed', 'dead', 'start', 'stop', 'stop-sigterm', 'stop-sigkill') else 'unknown', result=state.get('Result') if state.get('Result') in ('success', 'exit-code', 'signal', 'core-dump', 'oom-kill', 'timeout', 'resources') else 'unknown', status=int(code) if re.fullmatch('[0-9]{1,3}', code) and int(code) <= 255 else None) +def closed_native_exception(error): + if isinstance(error, FileNotFoundError): + return 'missing_file' + if isinstance(error, PermissionError): + return 'permission' + if isinstance(error, (TimeoutError, subprocess.TimeoutExpired)): + return 'timeout' + if isinstance(error, OSError): + return 'os_error' + if isinstance(error, subprocess.SubprocessError): + return 'subprocess_error' + if isinstance(error, ValueError): + return 'deadline' if str(error) == 'native_deadline' else 'contract' + return 'invalid_shape' + + def native_memory(limit): value = memory(NATIVE_UNIT) group = cgroup(NATIVE_UNIT) @@ -342,6 +360,98 @@ def native_memory(limit): return value +def native_helper_state(report): + state = properties(NATIVE_UNIT) + report['native_build_unit'] = closed_native_unit(state) + return state + + +def native_helper_live(state, expected_pid=None): + raw = state.get('MainPID', '') + require(state.get('ActiveState') == 'active' and state.get('SubState') == 'running' + and re.fullmatch('[0-9]{1,10}', raw) and 1 < int(raw) < 2 ** 31, + 'native_helper_not_running') + pid = int(raw) + require(expected_pid is None or pid == expected_pid, 'native_helper_changed') + group = cgroup(NATIVE_UNIT) + require(group.is_dir(), 'native_cgroup_missing') + files = [] + for path in group.rglob('cgroup.procs'): + require(len(files) < 16, 'native_cgroup_shape') + files.append(path) + require(files and group / 'cgroup.procs' in files, 'native_cgroup_shape') + processes = [] + for path in files: + with path.open('rb') as stream: + raw = stream.read(4097) + require(len(raw) <= 4096 and all(re.fullmatch(rb'[0-9]{1,10}', row) for row in raw.splitlines()), + 'native_cgroup_shape') + processes.extend(int(row) for row in raw.splitlines()) + require(processes == [pid], 'native_build_descendants') + return pid + + +def native_build_handshake(native, listener, nonce, deadline, limit, report): + report['native_build_stage'] = 'ready' + while True: + listener.settimeout(min(.2, native.remaining(deadline))) + try: + channel, _ = listener.accept() + break + except TimeoutError: + state = native_helper_state(report) + require(state.get('ActiveState') == 'active' and state.get('SubState') == 'running', + 'native_build_ended_before_ready') + with channel: + helper_pid = native_helper_live(native_helper_state(report)) + native.peer(channel, helper_pid) + raw = native.receive_frame(channel, deadline) + report['native_build_stage'] = 'provenance' + build = native.validate_build(SOURCE, BASE) + require(raw == native.binding('ready', nonce, build['build_manifest_sha256']), 'native_handshake_binding') + native.remaining(deadline) + report['native_build_stage'] = 'resources' + native_helper_live(native_helper_state(report), helper_pid) + measured = native_memory(limit) + native_helper_live(native_helper_state(report), helper_pid) + report['native_build_memory'] = measured + report['native_build_stage'] = 'release' + native.send_frame(channel, native.binding('release', nonce, build['build_manifest_sha256']), deadline) + report['native_build_stage'] = 'exit' + while True: + native.remaining(deadline) + state = native_helper_state(report) + if state.get('SubState') == 'exited' or state.get('ActiveState') in ('failed', 'inactive'): + break + time.sleep(min(.1, native.remaining(deadline))) + require(state.get('SubState') == 'exited' and state.get('Result') == 'success' + and state.get('ExecMainStatus') == '0' and empty(NATIVE_UNIT), 'native_build_failed') + native.remaining(deadline) + report['native_build_stage'] = 'complete' + return build + + +def run_native_build(limit, report): + native = module(INPUT / 'code/scripts/opencode_native_cpu.py', 'opencode_native_inputs') + nonce, deadline = os.urandom(32).hex(), time.monotonic_ns() + 1800 * 1_000_000_000 + path = BASE / native.CONTROL + report['native_build_stage'] = 'launch' + try: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + listener.bind(str(path)) # Existing socket/path is a failure, never replaced. + path.chmod(0o600) + listener.listen(1) + start(NATIVE_UNIT, [sys.executable, '-B', str(INPUT / 'code/scripts/opencode_native_cpu.py'), + '--execute', '--deadline-monotonic-ns', str(deadline), '--handshake-nonce', nonce, + '--parent-pid', str(os.getpid())], limit, 1800, public_source_build=True) + return native_build_handshake(native, listener, nonce, deadline, limit, report) + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as error: + report['native_build_exception'] = closed_native_exception(error) + raise + # The private socket remains inside BASE until the existing joined cleanup; + # no temporary handshake path can escape or weaken the all-units-empty gate. + + def start(name, command, limit, seconds, public_source_build=False): require(not public_source_build or name == NATIVE_UNIT, 'native_build_network_scope') (BASE / (name + '.log')).touch(mode=0o600) @@ -405,20 +515,7 @@ def guest(args): report['phase'] = 'native-source-build' require(properties(NATIVE_UNIT).get('LoadState') == 'not-found', 'existing_unit') created.append(NATIVE_UNIT) - start(NATIVE_UNIT, [sys.executable, '-B', str(INPUT / 'code/scripts/opencode_native_cpu.py'), - '--execute'], profile['core_memory_bytes'], 1800, public_source_build=True) - deadline = time.monotonic() + 1815 - while time.monotonic() < deadline: - native_state = properties(NATIVE_UNIT) - if native_state.get('SubState') == 'exited' or native_state.get('ActiveState') in ('failed', 'inactive'): - break - time.sleep(2) - report['native_build_unit'] = closed_native_unit(native_state) - require(native_state.get('SubState') == 'exited' and native_state.get('Result') == 'success' - and native_state.get('ExecMainStatus') == '0' and empty(NATIVE_UNIT), 'native_build_failed') - report['native_build_memory'] = native_memory(profile['core_memory_bytes']) - native = module(INPUT / 'code/scripts/opencode_native_cpu.py', 'opencode_native_inputs') - report['native_build'] = native.validate_build(SOURCE, BASE) + report['native_build'] = run_native_build(profile['core_memory_bytes'], report) report['phase'] = 'model-provision' provision_stage = 'pins' model = module(private.ML / 'provision.py', 'opencode_model_provision') diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index fd795f3..f129069 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -7,9 +7,13 @@ import json import os from pathlib import Path +import socket import subprocess +import sys import tarfile import tempfile +import threading +import time from types import SimpleNamespace import unittest from unittest.mock import patch @@ -292,10 +296,313 @@ def test_native_conversion_diagnostic_is_exact_opt_in_and_no_raw_export(self): def test_native_unit_diagnostics_are_closed_and_do_not_invent_log_inspection(self): value = TRIAL.closed_native_unit({'ActiveState': 'failed', 'Result': 'timeout', - 'ExecMainStatus': '9', 'private': 'CANARY'}) - self.assertEqual(value, {'active': 'failed', 'result': 'timeout', 'status': 9}) + 'SubState': 'failed', 'ExecMainStatus': '9', 'private': 'CANARY'}) + self.assertEqual(value, {'active': 'failed', 'substate': 'failed', 'result': 'timeout', 'status': 9}) self.assertNotIn('CANARY', json.dumps(value)) self.assertNotIn('stderr', json.dumps(value)) + self.assertEqual(TRIAL.closed_native_unit({'SubState': 'CANARY'})['substate'], 'unknown') + for error, expected in ((FileNotFoundError('CANARY'), 'missing_file'), + (PermissionError('CANARY'), 'permission'), + (TimeoutError('CANARY'), 'timeout'), + (subprocess.TimeoutExpired(['CANARY'], 1), 'timeout'), + (subprocess.CalledProcessError(1, ['CANARY']), 'subprocess_error'), + (OSError('CANARY'), 'os_error'), + (ValueError('native_deadline'), 'deadline'), + (ValueError('CANARY'), 'contract'), (KeyError('CANARY'), 'invalid_shape')): + self.assertEqual(TRIAL.closed_native_exception(error), expected) + + def test_original_post_exit_resource_read_reproduces_pruned_cgroup_failure(self): + with tempfile.TemporaryDirectory() as temp, patch.object(TRIAL, 'cgroup', return_value=Path(temp) / 'pruned'): + state = dict(ActiveState='active', SubState='exited', Result='success', ExecMainStatus='0') + self.assertEqual(TRIAL.closed_native_unit(state)['status'], 0) + self.assertTrue(TRIAL.empty(TRIAL.NATIVE_UNIT)) + with self.assertRaises(FileNotFoundError): + TRIAL.native_memory(11 * TRIAL.GIB) + + def exercise_handshake(self, *, ready_nonce=None, terminal_failure=False, resources=None, + pruned=False, descendants=False, stale_manifest=False, expire_after_snapshot=False, + ready_payload=None): + """Real local sockets, synthetic cgroup/unit data; never a host service/build/model.""" + with tempfile.TemporaryDirectory(prefix='vp-native-') as temp: + root, limit = Path(temp), 11 * TRIAL.GIB + group = root / 'group' + group.mkdir() + values = {'cgroup.procs': str(os.getpid()) + '\n', 'memory.max': str(limit), + 'memory.swap.max': '0', 'memory.peak': '123456', 'memory.swap.current': '0', + 'memory.events': 'low 0\nhigh 0\nmax 0\noom 0\noom_kill 0\n'} + if descendants: + values['cgroup.procs'] += str(os.getpid() + 1) + '\n' + values.update(resources or {}) + for name, value in values.items(): + (group / name).write_text(value) + build = {'build_manifest_sha256': 'a' * 64} + nonce = 'b' * 64 + done, errors, events = threading.Event(), [], [] + expired = threading.Event() + clock = time.monotonic_ns + deadline = time.monotonic_ns() + 2_000_000_000 + def helper(): + try: + if ready_payload is None: + NATIVE.await_release(root, build, ready_nonce or nonce, deadline, os.getpid()) + else: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as channel: + channel.settimeout(NATIVE.remaining(deadline)) + channel.connect(str(root / NATIVE.CONTROL)) + NATIVE.send_frame(channel, ready_payload, deadline) + NATIVE.receive_frame(channel, deadline) + events.append('helper_released') + except Exception as error: + errors.append(error) + finally: + done.set() + def state(_name): + if done.is_set(): + (group / 'cgroup.procs').write_text('') + return dict(ActiveState='failed' if terminal_failure else 'active', + SubState='failed' if terminal_failure else 'exited', + MainPID='0', Result='exit-code' if terminal_failure else 'success', + ExecMainStatus='7' if terminal_failure else '0') + return dict(ActiveState='active', SubState='running', MainPID=str(os.getpid()), + Result='success', ExecMainStatus='0') + original_memory = TRIAL.native_memory + def observe(current_limit): + self.assertFalse(done.is_set()) + self.assertNotIn('helper_released', events) + result = original_memory(current_limit) + events.append('parent_measured') + if expire_after_snapshot: + expired.set() + return result + report, failure, result = {}, None, None + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + path = root / NATIVE.CONTROL + listener.bind(str(path)) + path.chmod(0o600) + listener.listen(1) + worker = threading.Thread(target=helper) + worker.start() + try: + with patch.object(TRIAL, 'properties', side_effect=state), \ + patch.object(TRIAL, 'cgroup', return_value=root / 'missing' if pruned else group), \ + patch.object(TRIAL, 'native_memory', side_effect=observe), \ + patch.object(NATIVE.time, 'monotonic_ns', side_effect=lambda: + deadline + 1 if expired.is_set() else clock()), \ + patch.object(NATIVE, 'validate_build', return_value=dict(build, + build_manifest_sha256='c' * 64 if stale_manifest else build['build_manifest_sha256'])): + result = TRIAL.native_build_handshake(NATIVE, listener, nonce, deadline, limit, report) + except Exception as error: + failure = error + finally: + worker.join(timeout=3) + self.assertFalse(worker.is_alive(), 'helper must join on every handshake failure') + return result, report, failure, errors, events + + def test_actual_socket_handshake_keeps_helper_live_for_parent_resource_snapshot(self): + result, report, failure, errors, events = self.exercise_handshake() + self.assertIsNone(failure) + self.assertEqual(errors, []) + self.assertEqual(events, ['parent_measured', 'helper_released']) + self.assertEqual(result, {'build_manifest_sha256': 'a' * 64}) + self.assertEqual(report['native_build_stage'], 'complete') + self.assertEqual(report['native_build_unit']['substate'], 'exited') + self.assertEqual(report['native_build_memory'], dict(maximum=11 * TRIAL.GIB, swap=0, + peak=123456, oom_kill=0, swap_current=0, oom=0)) + + def test_separate_helper_process_uses_actual_peer_credentials_and_joins(self): + # The real handshake crosses a process boundary. Only unit/cgroup and + # build provenance are inert fixtures; no systemd, source build or model. + with tempfile.TemporaryDirectory(prefix='vp-native-') as temp, \ + socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + root, limit = Path(temp), 11 * TRIAL.GIB + group = root / 'group' + group.mkdir() + for name, value in {'memory.max': str(limit), 'memory.swap.max': '0', + 'memory.peak': '123456', 'memory.swap.current': '0', + 'memory.events': 'oom 0\noom_kill 0\n'}.items(): + (group / name).write_text(value) + path = root / NATIVE.CONTROL + listener.bind(str(path)) + path.chmod(0o600) + listener.listen(1) + deadline = time.monotonic_ns() + 3_000_000_000 + nonce, build = 'b' * 64, {'build_manifest_sha256': 'a' * 64} + program = '''import importlib.util, sys +from pathlib import Path +spec = importlib.util.spec_from_file_location('inert_native_helper', sys.argv[1]) +value = importlib.util.module_from_spec(spec) +spec.loader.exec_module(value) +value.await_release(Path(sys.argv[2]), {'build_manifest_sha256': 'a' * 64}, + sys.argv[3], int(sys.argv[4]), int(sys.argv[5])) +''' + child = subprocess.Popen([sys.executable, '-B', '-c', program, + str(ROOT / 'scripts/opencode_native_cpu.py'), str(root), nonce, str(deadline), str(os.getpid())], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (group / 'cgroup.procs').write_text(str(child.pid) + '\n') + def state(_name): + code = child.poll() + if code is not None: + (group / 'cgroup.procs').write_text('') + return dict(ActiveState='active' if code == 0 else 'failed', + SubState='exited' if code == 0 else 'failed', + MainPID='0', Result='success' if code == 0 else 'exit-code', ExecMainStatus=str(code)) + return dict(ActiveState='active', SubState='running', MainPID=str(child.pid), + Result='success', ExecMainStatus='0') + try: + with patch.object(TRIAL, 'properties', side_effect=state), \ + patch.object(TRIAL, 'cgroup', return_value=group), \ + patch.object(NATIVE, 'validate_build', return_value=build): + report = {} + self.assertEqual(TRIAL.native_build_handshake(NATIVE, listener, nonce, deadline, limit, report), build) + self.assertEqual(report['native_build_stage'], 'complete') + stdout, stderr = child.communicate(timeout=1) + self.assertEqual((child.returncode, stdout, stderr), (0, b'', b'')) + finally: + if child.poll() is None: + child.kill() + child.communicate(timeout=2) + + def test_ready_binding_or_manifest_mismatch_never_releases_helper(self): + for options in ({'ready_nonce': 'c' * 64}, {'stale_manifest': True}, + {'ready_payload': b'{"unknown":"CANARY"}\n'}, {'ready_payload': b'invalid\n'}): + with self.subTest(options=options): + result, report, failure, errors, events = self.exercise_handshake(**options) + self.assertIsNone(result) + self.assertRegex(str(failure), 'native_handshake_binding') + self.assertTrue(errors) + self.assertEqual(events, []) + self.assertNotIn('native_build_memory', report) + + def test_expiry_after_valid_snapshot_still_refuses_release_and_success(self): + result, report, failure, errors, events = self.exercise_handshake(expire_after_snapshot=True) + self.assertIsNone(result) + self.assertRegex(str(failure), 'native_deadline') + self.assertEqual(events, ['parent_measured']) + self.assertTrue(errors) + self.assertEqual(report['native_build_stage'], 'release') + + def test_pruned_cgroup_or_remaining_descendants_cannot_pass_readiness(self): + for options in ({'pruned': True}, {'descendants': True}): + with self.subTest(options=options): + result, report, failure, errors, events = self.exercise_handshake(**options) + self.assertIsNone(result) + self.assertIsInstance(failure, ValueError) + self.assertEqual(events, []) + self.assertTrue(errors) + self.assertNotIn('native_build_memory', report) + + def test_nonzero_actual_swap_or_oom_never_releases_helper(self): + for mutation in ({'memory.swap.current': '1'}, {'memory.swap.max': '1'}, + {'memory.events': 'oom 1\noom_kill 0\n'}, + {'memory.events': 'oom 0\noom_kill 1\n'}, + {'memory.max': str(12 * TRIAL.GIB)}, {'memory.peak': str(12 * TRIAL.GIB)}): + with self.subTest(mutation=mutation): + result, report, failure, errors, events = self.exercise_handshake(resources=mutation) + self.assertIsNone(result) + self.assertRegex(str(failure), 'native_build_resource_violation') + self.assertEqual(events, []) + self.assertTrue(errors) + self.assertEqual(report['native_build_stage'], 'resources') + + def test_parent_measurement_does_not_mask_failed_exit_after_release(self): + result, report, failure, errors, events = self.exercise_handshake(terminal_failure=True) + self.assertIsNone(result) + self.assertRegex(str(failure), 'native_build_failed') + self.assertEqual(errors, []) + self.assertEqual(events, ['parent_measured', 'helper_released']) + self.assertEqual(report['native_build_stage'], 'exit') + self.assertEqual(report['native_build_unit']['status'], 7) + + def test_release_mismatch_is_refused_by_actual_helper_socket(self): + with tempfile.TemporaryDirectory(prefix='vp-native-') as temp: + root, errors = Path(temp), [] + deadline = time.monotonic_ns() + 2_000_000_000 + def helper(): + try: + NATIVE.await_release(root, {'build_manifest_sha256': 'a' * 64}, + 'b' * 64, deadline, os.getpid()) + except Exception as error: + errors.append(error) + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + path = root / NATIVE.CONTROL + listener.bind(str(path)) + path.chmod(0o600) + listener.listen(1) + listener.settimeout(2) + worker = threading.Thread(target=helper) + worker.start() + try: + channel, _ = listener.accept() + with channel: + self.assertEqual(NATIVE.receive_frame(channel, deadline), NATIVE.binding('ready', 'b' * 64, 'a' * 64)) + NATIVE.send_frame(channel, NATIVE.binding('release', 'c' * 64, 'a' * 64), deadline) + finally: + worker.join(timeout=3) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(errors), 1) + self.assertRegex(str(errors[0]), 'native_handshake_binding') + + def test_expired_unknown_peer_and_oversized_or_empty_socket_frames_fail_closed(self): + now = time.monotonic_ns() + for deadline in (now - 1, now + 1801 * 1_000_000_000, True, None): + with self.subTest(deadline=deadline), self.assertRaisesRegex(ValueError, 'native_deadline'): + NATIVE.remaining(deadline) + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + with self.assertRaisesRegex(ValueError, 'native_deadline'): + TRIAL.native_build_handshake(NATIVE, listener, 'b' * 64, now - 1, 11 * TRIAL.GIB, {}) + for payload in (b'', b'x' * 1025): + left, right = socket.socketpair() + with self.subTest(size=len(payload)), left, right: + NATIVE.peer(left, os.getpid()) + with self.assertRaisesRegex(ValueError, 'native_handshake_peer'): + NATIVE.peer(left, os.getpid() + 1) + right.sendall(payload) + right.shutdown(socket.SHUT_WR) + with self.assertRaisesRegex(ValueError, 'native_handshake_frame'): + NATIVE.receive_frame(left, time.monotonic_ns() + 1_000_000_000) + left, right = socket.socketpair() + with left, right, self.assertRaises(TimeoutError): + NATIVE.receive_frame(left, time.monotonic_ns() + 10_000_000) + + def test_missing_readiness_after_unit_failure_is_not_waited_into_success(self): + with tempfile.TemporaryDirectory(prefix='vp-native-') as temp, \ + socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + listener.bind(str(Path(temp) / 'control')) + listener.listen(1) + state = dict(ActiveState='failed', SubState='failed', MainPID='0', Result='exit-code', ExecMainStatus='1') + report = {} + with patch.object(TRIAL, 'properties', return_value=state), \ + self.assertRaisesRegex(ValueError, 'native_build_ended_before_ready'): + TRIAL.native_build_handshake(NATIVE, listener, 'b' * 64, + time.monotonic_ns() + 1_000_000_000, 11 * TRIAL.GIB, report) + self.assertEqual(report['native_build_unit']['status'], 1) + + def test_launch_uses_one_original_deadline_and_closes_listener_on_errors(self): + with tempfile.TemporaryDirectory(prefix='vp-native-') as temp, \ + patch.object(TRIAL, 'BASE', Path(temp)), patch.object(TRIAL, 'module', return_value=NATIVE), \ + patch.object(TRIAL, 'start') as start: + report, before = {}, time.monotonic_ns() + def fail(native, listener, nonce, deadline, limit, observed): + self.assertIs(native, NATIVE) + self.assertIs(observed, report) + self.assertEqual(limit, 11 * TRIAL.GIB) + self.assertLessEqual(deadline, time.monotonic_ns() + 1800 * 1_000_000_000) + self.assertGreaterEqual(deadline, before + 1800 * 1_000_000_000) + unit, args, memory, seconds = start.call_args.args + self.assertEqual((unit, memory, seconds), (TRIAL.NATIVE_UNIT, limit, 1800)) + self.assertEqual(start.call_args.kwargs, {'public_source_build': True}) + self.assertEqual(args[-6:], ['--deadline-monotonic-ns', str(deadline), + '--handshake-nonce', nonce, '--parent-pid', str(os.getpid())]) + self.assertEqual(Path(listener.getsockname()), Path(temp) / NATIVE.CONTROL) + raise FileNotFoundError('CANARY') + with patch.object(TRIAL, 'native_build_handshake', side_effect=fail), self.assertRaises(FileNotFoundError): + TRIAL.run_native_build(11 * TRIAL.GIB, report) + self.assertEqual(report['native_build_exception'], 'missing_file') + self.assertNotIn('CANARY', json.dumps(report)) + # Existing control endpoints are never overwritten/reused. + with self.assertRaises(OSError): + TRIAL.run_native_build(11 * TRIAL.GIB, {}) + self.assertEqual(start.call_count, 1) def bundle_fixture(self, directory): base, core = Path(directory) / 'base', Path(directory) / 'core' From cf33cba11396f70cde03db27852b616502b2cbd7 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:11:53 +0200 Subject: [PATCH 09/39] code: preserve closed conversion failure diagnostics --- docs/OPENCODE.md | 26 ++++++++- scripts/smoke_opencode_inference.py | 46 ++++++++++++++- tests/test_opencode_native_cpu.py | 89 ++++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 6 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 74ca517..c52d805 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -723,7 +723,7 @@ stall's cause. Original artifact ZIP SHA-256: An explicitly selected `--inference-backend llama_cpp_bf16_v1` now prepares a different CPU executor for that same original 4B task. It pins core -`7308371b20ced0504662178beb0e46586cfc9d2d` and llama.cpp source +`41c2f66437140360f09e706fb684d6f3046ac514` and llama.cpp source `7fe450e19305b828c199d602c23a8337aaa1f03b`. The default `torch` backend and both existing model profiles retain their previous core pins. Native selection is refused for the smaller profile; there is no silent backend fallback. @@ -759,8 +759,28 @@ helper PID and actual cgroup counters. Only then may it exit, still requiring status `0` and an empty unit. The same absolute 1,800-second build deadline includes the handshake. Missing/mismatched/late messages, unavailable counters and failed exits remain failures. Closed substate, stage and exception-category -diagnostics expose no raw logs or private paths. This correction is not yet -verified by another actual native trial. +diagnostics expose no raw logs or private paths. + +The original follow-up [trial37614132814](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37614132814) +on Code `01c5c591b16ed34226341e04fe673a35a7a169de` and core `7308371b` +now proves that source build and handshake complete: a verified library/manifest, +successful exit and observed build peak of 1,031,565,312 bytes with zero swap/OOM. +The trial nevertheless **failed** at `model-provision` with `native_conversion`, +process status 1. Wheel-graph and runtime-import checks passed first; the generic +conversion marker does not preserve the failed substage or exception. Those +build counters are not conversion resource measurements. No inference or coding +task was reached. All reported private-data, unit, process-group, QEMU and scratch +cleanup checks pass, with scoped routes/DNS unchanged. Original ZIP SHA-256: +`d4b32d66e90d4c76568b2fa989cbab5e49bbb736283868a03a81f9ae0303a254`; +job `112768290137` log SHA-256: +`6e561f75a15560a56f3d98c1a49e518c52ad543646ea1463c7f475e5ec2d0ce6`. +The follow-up adds bounded conversion diagnostics without exporting raw logs, +paths or model content; it is not a demonstrated conversion fix. +Native-only detail distinguishes absent, invalid, unknown and reported failure; +it cannot report success. Duplicate fields/markers, malformed values and a +claimed child failure without a nonzero integer exit status are rejected. +All sixty-one native, smoke and CI fixture checks pass, independently repeated +by root. Default Torch summaries and all trial limits remain unchanged. Select the native backend consistently for source selection, packing and execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index c627b3a..e7a15a0 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native converter/process cleanup plus the reviewed unique CLI argument group. -NATIVE_CORE = '7308371b20ced0504662178beb0e46586cfc9d2d' +NATIVE_CORE = '41c2f66437140360f09e706fb684d6f3046ac514' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' @@ -110,6 +110,47 @@ def load(path, maximum=2 * 1024**2): return json.loads(path.read_bytes()) +def closed_conversion(lines, log_state, process_status): + """A diagnostic can report failure, never establish successful conversion.""" + value = dict(state='absent', stage='unknown', failure='unknown', child_exit_status=None) + if log_state != 'present': + value['state'] = 'absent' if log_state == 'absent' else 'invalid' + return value + prefix = b'NATIVE_CONVERSION_DIAGNOSTIC ' + rows = [line[len(prefix):] for line in lines if line.startswith(prefix)] + if not rows: + return value + value['state'] = 'invalid' + if len(rows) != 1 or len(rows[0]) > 1024 or lines.count(b'NATIVE_CONVERSION_FAILED') != 1 \ + or type(process_status) is not int or not -128 <= process_status <= 255 or process_status == 0: + return value + def unique(pairs): + result = {} + for key, item in pairs: + require(key not in result, 'duplicate_conversion_field') + result[key] = item + return result + try: + row = json.loads(rows[0], object_pairs_hook=unique) + stages = {'guard', 'source', 'runtime', 'model', 'build', 'budget', 'convert', 'tokenizer', + 'verify', 'recheck', 'bundle', 'manifest', 'permissions', 'unknown'} + failures = {'cancelled', 'timeout', 'missing_file', 'permission', 'memory', 'import', 'subprocess', + 'os_error', 'invalid_shape', 'child_failed', 'disk_budget', 'contract', 'unknown'} + require(type(row) is dict and set(row) == {'version', 'kind', 'stage', 'failure', 'child_exit_status'} + and type(row['version']) is int and row['version'] == 1 + and row['kind'] == 'native-conversion-failure' + and type(row['stage']) is str and row['stage'] in stages + and type(row['failure']) is str and row['failure'] in failures + and (row['child_exit_status'] is None or (type(row['child_exit_status']) is int + and -128 <= row['child_exit_status'] <= 255)), 'conversion_diagnostic_shape') + require(row['failure'] != 'child_failed' or (type(row['child_exit_status']) is int + and row['child_exit_status'] != 0), 'conversion_child_status') + return dict(state='unknown' if 'unknown' in (row['stage'], row['failure']) else 'reported_failure', + **{key: row[key] for key in ('stage', 'failure', 'child_exit_status')}) + except (ValueError, TypeError, KeyError, UnicodeError): + return value + + def closed_provision(path, pins, stage, returncode, wait_timeout=False, native_cpu=False): """Bounded metadata only; progress means download starts, not verified assets.""" stages = {'pins', 'launch', 'process', 'report', 'provenance', 'complete'} @@ -119,6 +160,7 @@ def closed_provision(path, pins, stage, returncode, wait_timeout=False, native_c download_starts=0, last_artifact_index=None, failure_class='unknown', http_status=None, wheel_graph_checked=False, runtime_import_checked=False) native_conversion_failed = False + lines = [] try: descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) with os.fdopen(descriptor, 'rb') as stream: @@ -190,6 +232,8 @@ def closed_provision(path, pins, stage, returncode, wait_timeout=False, native_c value['log_state'] = 'invalid' if native_conversion_failed and value['log_state'] == 'present': value['failure_class'] = 'native_conversion' + if native_cpu is True: + value['native_conversion'] = closed_conversion(lines, value['log_state'], value['process_status']) return value diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index f129069..8f3651f 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -36,7 +36,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '7308371b20ced0504662178beb0e46586cfc9d2d') + self.assertEqual(TRIAL.NATIVE_CORE, '41c2f66437140360f09e706fb684d6f3046ac514') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): @@ -284,7 +284,8 @@ def test_native_conversion_diagnostic_is_exact_opt_in_and_no_raw_export(self): path.chmod(0o600) legacy = TRIAL.closed_provision(path, None, 'process', 1) native = TRIAL.closed_provision(path, None, 'process', 1, native_cpu=True) - self.assertEqual(set(legacy), set(native)) + self.assertEqual(set(legacy) | {'native_conversion'}, set(native)) + self.assertEqual(native['native_conversion']['state'], 'absent') self.assertEqual(legacy['failure_class'], 'runtime_subprocess') self.assertEqual(native['failure_class'], 'native_conversion') self.assertNotIn('CANARY', json.dumps(native)) @@ -294,6 +295,90 @@ def test_native_conversion_diagnostic_is_exact_opt_in_and_no_raw_export(self): self.assertNotEqual(TRIAL.closed_provision(path, None, 'process', 1, native_cpu=True) ['failure_class'], 'native_conversion') + @staticmethod + def conversion_line(**changes): + value = dict(version=1, kind='native-conversion-failure', stage='convert', + failure='child_failed', child_exit_status=7) + value.update(changes) + return b'NATIVE_CONVERSION_DIAGNOSTIC ' + json.dumps(value).encode() + + def test_conversion_failure_schema_preserves_closed_stage_and_child_status(self): + stages = ('guard', 'source', 'runtime', 'model', 'build', 'budget', 'convert', 'tokenizer', + 'verify', 'recheck', 'bundle', 'manifest', 'permissions') + failures = ('cancelled', 'timeout', 'missing_file', 'permission', 'memory', 'import', + 'subprocess', 'os_error', 'invalid_shape', 'child_failed', 'disk_budget', 'contract') + for stage in stages: + for failure in failures: + with self.subTest(stage=stage, failure=failure): + value = TRIAL.closed_conversion([self.conversion_line(stage=stage, failure=failure), + b'NATIVE_CONVERSION_FAILED'], 'present', 1) + self.assertEqual(value, dict(state='reported_failure', stage=stage, + failure=failure, child_exit_status=7)) + for status in (-128, -15, -9, 0, 255, None): + with self.subTest(status=status): + value = TRIAL.closed_conversion([self.conversion_line(failure='timeout', child_exit_status=status), + b'NATIVE_CONVERSION_FAILED'], 'present', 1) + self.assertEqual(value['child_exit_status'], status) + self.assertEqual(value['state'], 'reported_failure') + + def test_conversion_absent_unknown_invalid_and_forged_success_stay_unproven(self): + for lines in ([], [b'NATIVE_CONVERSION_FAILED'], [b'prefix ' + self.conversion_line()]): + self.assertEqual(TRIAL.closed_conversion(lines, 'present', 1)['state'], 'absent') + for changes in ({'stage': 'unknown'}, {'failure': 'unknown'}): + result = TRIAL.closed_conversion([self.conversion_line(**changes), b'NATIVE_CONVERSION_FAILED'], 'present', 1) + self.assertEqual(result['state'], 'unknown') + for changes in ({'version': True}, {'version': 2}, {'kind': 'native-conversion-success'}, + {'stage': 'complete'}, {'stage': ['CANARY']}, {'stage': 'CANARY'}, + {'failure': 'success'}, {'failure': 'CANARY'}, {'child_exit_status': True}, + {'child_exit_status': -129}, {'child_exit_status': 256}, {'child_exit_status': '7'}, + {'child_exit_status': 0}, {'child_exit_status': None}, {'private_path': 'CANARY'}): + result = TRIAL.closed_conversion([self.conversion_line(**changes), b'NATIVE_CONVERSION_FAILED'], 'present', 1) + self.assertEqual(result, dict(state='invalid', stage='unknown', failure='unknown', child_exit_status=None)) + self.assertNotIn('CANARY', json.dumps(result)) + for status in (0, None, True, 256): + result = TRIAL.closed_conversion([self.conversion_line(), b'NATIVE_CONVERSION_FAILED'], 'present', status) + self.assertEqual(result['state'], 'invalid') + for state, expected in (('absent', 'absent'), ('invalid', 'invalid'), ('truncated', 'invalid')): + self.assertEqual(TRIAL.closed_conversion([self.conversion_line(), b'NATIVE_CONVERSION_FAILED'], + state, 1)['state'], expected) + + def test_conversion_duplicate_malformed_and_missing_markers_are_rejected(self): + valid = self.conversion_line() + mutations = ([valid], [valid, valid, b'NATIVE_CONVERSION_FAILED'], + [valid, b'NATIVE_CONVERSION_FAILED', b'NATIVE_CONVERSION_FAILED'], + [valid.replace(b'"version": 1', b'"version": 0, "version": 1'), b'NATIVE_CONVERSION_FAILED'], + [b'NATIVE_CONVERSION_DIAGNOSTIC {', b'NATIVE_CONVERSION_FAILED'], + [b'NATIVE_CONVERSION_DIAGNOSTIC []', b'NATIVE_CONVERSION_FAILED'], + [b'NATIVE_CONVERSION_DIAGNOSTIC null', b'NATIVE_CONVERSION_FAILED'], + [b'NATIVE_CONVERSION_DIAGNOSTIC "\\ud800"', b'NATIVE_CONVERSION_FAILED'], + [b'NATIVE_CONVERSION_DIAGNOSTIC ' + b'x' * 1025, b'NATIVE_CONVERSION_FAILED'], + [b'NATIVE_CONVERSION_DIAGNOSTIC \xff', b'NATIVE_CONVERSION_FAILED']) + for lines in mutations: + with self.subTest(lines=lines): + self.assertEqual(TRIAL.closed_conversion(lines, 'present', 1), + dict(state='invalid', stage='unknown', failure='unknown', child_exit_status=None)) + + def test_conversion_detail_flows_into_final_provision_summary_only_when_selected(self): + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / 'provision.log' + raw = self.conversion_line(stage='verify', failure='contract', child_exit_status=0) \ + + b'\nNATIVE_CONVERSION_FAILED\nProvisioning refused: Command CANARY returned non-zero exit status 1.\n' + path.write_bytes(raw) + path.chmod(0o600) + native = TRIAL.closed_provision(path, None, 'process', 1, native_cpu=True) + self.assertEqual(native['failure_class'], 'native_conversion') + self.assertEqual(native['native_conversion'], dict(state='reported_failure', stage='verify', + failure='contract', child_exit_status=0)) + self.assertNotIn('CANARY', json.dumps(native)) + self.assertNotIn('native_conversion', TRIAL.closed_provision(path, None, 'process', 1)) + path.write_bytes(raw + b'x' * 131073) + self.assertEqual(TRIAL.closed_provision(path, None, 'process', 1, native_cpu=True) + ['native_conversion']['state'], 'invalid') + path.write_bytes(raw + self.conversion_line() + b'\n') + native = TRIAL.closed_provision(path, None, 'process', 1, native_cpu=True) + self.assertEqual(native['failure_class'], 'native_conversion') + self.assertEqual(native['native_conversion']['state'], 'invalid') + def test_native_unit_diagnostics_are_closed_and_do_not_invent_log_inspection(self): value = TRIAL.closed_native_unit({'ActiveState': 'failed', 'Result': 'timeout', 'SubState': 'failed', 'ExecMainStatus': '9', 'private': 'CANARY'}) From 61d2c0c52468eee7728cf6f32a107baca5bae240 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:17:22 +0200 Subject: [PATCH 10/39] code: bind native diagnostics to integrated core source --- docs/OPENCODE.md | 2 +- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index c52d805..4e5dad0 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -723,7 +723,7 @@ stall's cause. Original artifact ZIP SHA-256: An explicitly selected `--inference-backend llama_cpp_bf16_v1` now prepares a different CPU executor for that same original 4B task. It pins core -`41c2f66437140360f09e706fb684d6f3046ac514` and llama.cpp source +`7739206361aa0aeb7cf0b20d9f8cf1cc9da5c81b` and llama.cpp source `7fe450e19305b828c199d602c23a8337aaa1f03b`. The default `torch` backend and both existing model profiles retain their previous core pins. Native selection is refused for the smaller profile; there is no silent backend fallback. diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index e7a15a0..a4f6330 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native converter/process cleanup plus the reviewed unique CLI argument group. -NATIVE_CORE = '41c2f66437140360f09e706fb684d6f3046ac514' +NATIVE_CORE = '7739206361aa0aeb7cf0b20d9f8cf1cc9da5c81b' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 8f3651f..a58b619 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -36,7 +36,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '41c2f66437140360f09e706fb684d6f3046ac514') + self.assertEqual(TRIAL.NATIVE_CORE, '7739206361aa0aeb7cf0b20d9f8cf1cc9da5c81b') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 8a7034d5e09cf34f8ac67742f347eb657f769440 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:41:39 +0200 Subject: [PATCH 11/39] test: pin native trial to source-matched Qwen3 metadata checks --- docs/OPENCODE.md | 23 ++++++++++++++++++++++- scripts/smoke_opencode_inference.py | 4 ++-- tests/test_opencode_native_cpu.py | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 4e5dad0..e5bee0c 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -723,7 +723,7 @@ stall's cause. Original artifact ZIP SHA-256: An explicitly selected `--inference-backend llama_cpp_bf16_v1` now prepares a different CPU executor for that same original 4B task. It pins core -`7739206361aa0aeb7cf0b20d9f8cf1cc9da5c81b` and llama.cpp source +`10fd16dc78a0c91dee627b9d8bdab2226f2cc415` and llama.cpp source `7fe450e19305b828c199d602c23a8337aaa1f03b`. The default `torch` backend and both existing model profiles retain their previous core pins. Native selection is refused for the smaller profile; there is no silent backend fallback. @@ -782,6 +782,27 @@ claimed child failure without a nonzero integer exit status are rejected. All sixty-one native, smoke and CI fixture checks pass, independently repeated by root. Default Torch summaries and all trial limits remain unchanged. +The subsequent [trial 37623495917](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37623495917) +on Code `61d2c0c52468eee7728cf6f32a107baca5bae240` and core +`7739206361aa0aeb7cf0b20d9f8cf1cc9da5c81b` also **failed**. The converter +child exited successfully, but verification reported `stage=verify` and +`failure=contract`; no verified model or inference followed. All reported +guest-unit, private-file, QEMU and scratch cleanup checks passed, with scoped +routes/DNS unchanged. The separate raw host-route byte comparison was false. +Original artifact ZIP SHA-256: +`50ad3ba19fe9811e132f2dad7ee3c49407d5fcecc86a65a816d7a0637eab996e`; +job `112799334236` log SHA-256: +`24bcefeb3279b24e011008fecca2037a0808dac2af82587527c1d378759c86a3`. + +The new core pin corrects one source-proven verifier mismatch: the pinned +Qwen3 converter omits `qwen3.rope.dimension_count`, and its loader uses the +validated attention key length when that optional value is absent. Verification +now follows the same rule while still rejecting an incorrect explicit rotary +length or any missing required model parameter. This is a targeted correction, +not retrospective proof of the first failed assertion or a successful conversion. +Tensor, tokenizer, prompt and resource checks remain unchanged. A fresh isolated +trial is required after both repositories pass their source checks. + Select the native backend consistently for source selection, packing and execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow exposes the same explicit choice. It must be registered and dispatched against diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index a4f6330..7019101 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -35,8 +35,8 @@ LARGE_CORE = '6a517b576baa17e7329661ee0476d1848081d114' MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') -# Native converter/process cleanup plus the reviewed unique CLI argument group. -NATIVE_CORE = '7739206361aa0aeb7cf0b20d9f8cf1cc9da5c81b' +# Native cleanup and source-matched Qwen3 rotary metadata verification. +NATIVE_CORE = '10fd16dc78a0c91dee627b9d8bdab2226f2cc415' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index a58b619..3148060 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -36,7 +36,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '7739206361aa0aeb7cf0b20d9f8cf1cc9da5c81b') + self.assertEqual(TRIAL.NATIVE_CORE, '10fd16dc78a0c91dee627b9d8bdab2226f2cc415') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From d48801a66f69b094b7f41d99240ee9d7418eb91c Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:30:16 +0200 Subject: [PATCH 12/39] test: pin owner-responsive full-weight verification candidate --- docs/OPENCODE.md | 34 ++++++++++++++++++++++++++--- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index e5bee0c..2ec9e3f 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -723,7 +723,7 @@ stall's cause. Original artifact ZIP SHA-256: An explicitly selected `--inference-backend llama_cpp_bf16_v1` now prepares a different CPU executor for that same original 4B task. It pins core -`10fd16dc78a0c91dee627b9d8bdab2226f2cc415` and llama.cpp source +`773d6377e5263c24a9263f7b97b5c9ae739b3f74` and llama.cpp source `7fe450e19305b828c199d602c23a8337aaa1f03b`. The default `torch` backend and both existing model profiles retain their previous core pins. Native selection is refused for the smaller profile; there is no silent backend fallback. @@ -800,8 +800,36 @@ validated attention key length when that optional value is absent. Verification now follows the same rule while still rejecting an incorrect explicit rotary length or any missing required model parameter. This is a targeted correction, not retrospective proof of the first failed assertion or a successful conversion. -Tensor, tokenizer, prompt and resource checks remain unchanged. A fresh isolated -trial is required after both repositories pass their source checks. +Tensor, tokenizer, prompt and resource checks remain unchanged. + +After both exact-source checks passed, [trial 37631532611](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37631532611) +on Code `8a7034d5e09cf34f8ac67742f347eb657f769440` and core +`10fd16dc78a0c91dee627b9d8bdab2226f2cc415` **failed during the OpenCode +task**. Source build, conversion and model provisioning now succeed, with a +verified 8,051,283,488-byte BF16 GGUF. The worker reports +`compute_control_ack_deadline` at `verify_files/begin`: an I/O-pressure pause +was not acknowledged while hashing the full original weights. Source review +found missing execution-thread checkpoints in that hash loop and subsequent +native verification. The new core pin processes controls between unchanged +1 MiB reads during preparation, native bundle validation and post-execution +rehashing. All 138 targeted core worker/native checks pass, independently +repeated, including real-file/owner-pipe pause, resume, cancel and unchanged +deadline tests. No integrity checks, resources or deadlines were relaxed. +A single blocking read can still exceed the control deadline; a new actual +large-model trial is required before claiming that execution succeeds. + +No native model turn or tool call completed. Six requests failed within this +single workflow, with cleanup confirmed for all six. Core peak memory was +2,303,844,352 bytes, with zero OOM kills and swap. All reported owned-unit, +process-group, private-data, QEMU and scratch cleanup passed; scoped host-state +files match. No raw model output was exported. Conversion is now demonstrated, +but inference, successful coding and protected peer execution remain unproved. +Original artifact ZIP SHA-256: +`78c4732774fc93dae9872df0460e2addf7f3849a6a190f803069fbc7d6a6e9c8`; +job `112826781182` log SHA-256: +`ef7d5c7bbeeebf497ec9a42881746a782b36f7c7bddc9febef11d7af373cb44f`. +The consumed dispatch receipts and all earlier failures remain unchanged; +there has been no repeat dispatch or main merge. Select the native backend consistently for source selection, packing and execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 7019101..8312cf2 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native cleanup and source-matched Qwen3 rotary metadata verification. -NATIVE_CORE = '10fd16dc78a0c91dee627b9d8bdab2226f2cc415' +NATIVE_CORE = '773d6377e5263c24a9263f7b97b5c9ae739b3f74' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 3148060..e7daebb 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -36,7 +36,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '10fd16dc78a0c91dee627b9d8bdab2226f2cc415') + self.assertEqual(TRIAL.NATIVE_CORE, '773d6377e5263c24a9263f7b97b5c9ae739b3f74') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 8b5d5b6ad2d764a503cffb508d5c176711c36840 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:14:06 +0200 Subject: [PATCH 13/39] fix(native): require bounded loader provenance from pinned core --- docs/OPENCODE.md | 45 +++++++- scripts/opencode_native_cpu.py | 31 ++++- scripts/smoke_opencode_inference.py | 4 +- tests/test_opencode_native_cpu.py | 168 +++++++++++++++++++++++++++- 4 files changed, 234 insertions(+), 14 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 2ec9e3f..abee79f 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -723,7 +723,7 @@ stall's cause. Original artifact ZIP SHA-256: An explicitly selected `--inference-backend llama_cpp_bf16_v1` now prepares a different CPU executor for that same original 4B task. It pins core -`773d6377e5263c24a9263f7b97b5c9ae739b3f74` and llama.cpp source +`f4cd8bd7ca310794dfc73cfcd096d6baedcf73bf` and llama.cpp source `7fe450e19305b828c199d602c23a8337aaa1f03b`. The default `torch` backend and both existing model profiles retain their previous core pins. Native selection is refused for the smaller profile; there is no silent backend fallback. @@ -831,6 +831,46 @@ job `112826781182` log SHA-256: The consumed dispatch receipts and all earlier failures remain unchanged; there has been no repeat dispatch or main merge. +The new owner-responsive verification [trial 37641148934](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37641148934), +attempt 1 on Code `d48801a6` / core `773d6377`, **fails** after build, conversion +and provisioning succeed with the same complete BF16 model. The worker now +acknowledges all 20 issued owner controls and reaches `model_load/begin`, then +reports `worker_other`. This closed diagnostic does not identify the precise +native load failure. No native turn, tool call or coding task completes; six +failed runtime requests have confirmed cleanup. + +Core cgroup peak is 8,106,295,296 bytes within the 11 GiB limit, with zero swap +and OOM kills. Owned units, processes, private data, QEMU and scratch cleanup +pass. Normalized route/DNS observations match; raw IPv6 route bytes differ in +the excluded counter column, not a byte-identical snapshot. No raw model output +was exported. Original artifact `11492792961` ZIP SHA-256: +`b2621c8918f1304848fcc4a7b9e736fab2379cdf19c29de9ab11606e5c9e9cfc`; +job `112860150266` log SHA-256: +`4e32650af9e373e5e28ec93663777f1fe833feee419a5fe604fcfdfa6b7ec438`. +This follow-up has its own consumed once-only receipts; no repeat was sent. + +The current source candidate bounds tensor validation in both upstream loading +paths to one execution thread, while still validating every tensor on success. +Owner controls are serviced between tensors; a single tensor remains +non-interruptible, and cancellation rejects the whole load. Closed native +load-stage diagnostics expose no raw model output or exception text. This +corrects a source-proven unbounded validation launch; it does not retrospectively +establish the cause of the preceding trial's failure. + +Code now requires the core's build-manifest v2, including original/effective +loader and patch provenance, and verifies the actual compiled loader source. +The core checkout stays unchanged. Its builder imports the exact sibling +contract afresh, without changing the module search path or retaining a +temporary import binding. Old v1 manifests, altered patches, mismatched compile +sources and changed copied build reports are rejected. The native library ABI +remains version 1; the build-manifest version is a separate contract. + +Focused inert checks cover the source-pinned import, provenance, tampering and +unchanged resource profile. A separate core source-only build passes its narrow +ABI and validation checks. Neither proves a full model load or successful coding +task on this candidate; a new separately authorized isolated trial is required. +The model, original prompt, precision, deadlines and memory limits are unchanged. + Select the native backend consistently for source selection, packing and execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow exposes the same explicit choice. It must be registered and dispatched against @@ -855,6 +895,7 @@ build-shell selector now forwards the explicit backend choice too; an inert shell-prefix regression reproduces the previous mismatch before any profile, privileged operation or build. Source-only CI also covers stacked pull requests, with unchanged checks and permissions. No actual -conversion, native model inference or successful coding loop has yet been proved. +native model inference or successful coding loop has yet been proved. Conversion +and provisioning are demonstrated by the separate real trials above. The core's retained full upstream sanitizer failure on a disallowed quantized path also remains explicit; separate BF16/F32 checks do not erase that failure. diff --git a/scripts/opencode_native_cpu.py b/scripts/opencode_native_cpu.py index 7f5c78e..0a45963 100644 --- a/scripts/opencode_native_cpu.py +++ b/scripts/opencode_native_cpu.py @@ -17,6 +17,7 @@ import stat import struct import subprocess +import sys import time SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' @@ -37,9 +38,27 @@ def require(value, reason): def module(path, name): + # The core archive is already source-pinned by the trial. Load its exact + # sibling contract, never a same-named module from sys.path or a prior core. + require(path.is_absolute() and path.resolve(strict=True) == path, 'native_module_path') + file_identity(path, 1024 ** 2) spec = importlib.util.spec_from_file_location(name, path) value = importlib.util.module_from_spec(spec) - spec.loader.exec_module(value) + if path.name == 'build_llama_cpu.py': + native = module(path.with_name('llama_cpu.py'), 'trial_native_contract') + absent = object() + previous = sys.modules.get('llama_cpu', absent) + try: + sys.modules['llama_cpu'] = native + spec.loader.exec_module(value) + require(getattr(value, 'native', None) is native, 'native_builder_contract') + finally: + if previous is absent: + sys.modules.pop('llama_cpu', None) + else: + sys.modules['llama_cpu'] = previous + else: + spec.loader.exec_module(value) return value @@ -121,7 +140,8 @@ def validate_build(core, base): require(builder.SOURCE == SOURCE and builder.ORIGIN == ORIGIN and builder.KIND == KIND, 'native_builder_pin') tree = builder.verify_source(base / 'llama-source') value, report_hash = document(base / 'llama-build/build.json', 65536) - require(value.get('version') == 1 and value.get('kind') == KIND and value.get('source_commit') == SOURCE + builder.native.validate_build_provenance(value, require) + require(value.get('kind') == KIND and value.get('source_commit') == SOURCE and value.get('source_tree') == tree and value.get('abi_version') == 1 and value.get('jobs') == 2 and value.get('cpu') == 'avx2_fma_f16c' and value.get('models_loaded') is False and value.get('downloads') is False @@ -131,8 +151,10 @@ def validate_build(core, base): library = file_identity(base / 'llama-build/libvolparossa_llama_cpu.so', 128 * 1024 ** 2) require(value.get('library') == {'path': 'libvolparossa_llama_cpu.so', **library}, 'native_library_binding') wrapper = {name: file_identity(core / 'workers/volparossa-ml/native-cpu' / name, 1024 ** 2) - for name in ('CMakeLists.txt', 'adapter.h', 'adapter.cpp')} + for name in ('CMakeLists.txt', 'adapter.h', 'adapter.cpp', 'bounded-tensor-validation.patch')} require(value.get('wrapper_sources') == wrapper, 'native_wrapper_binding') + builder.verify_loader_compilation(base / 'llama-build', + base / 'llama-build/loader-overlay/llama-model-loader.cpp') require(type(value.get('dynamic_dependencies')) is list and value['dynamic_dependencies'] and len(set(value['dynamic_dependencies'])) == len(value['dynamic_dependencies']) and set(value['dynamic_dependencies']) <= builder.ALLOWED_NEEDED, 'native_dependency_binding') @@ -157,8 +179,9 @@ def validate_provision(core, base, report, build): require(manifest_hash == backend['backend_sha256'] and value['model_profile'] == MODEL and value['build_manifest_sha256'] == build['build_manifest_sha256'] and value['library'] == {'path': 'libvolparossa_llama_cpu.so', **build['library']}, 'native_manifest_binding') - _, build_hash = document(root / 'build.json', 65536) + copied_build, build_hash = document(root / 'build.json', 65536) require(build_hash == build['build_manifest_sha256'], 'native_copied_build_binding') + native.validate_build_provenance(copied_build, require) for name, maximum in (('library', 128 * 1024 ** 2), ('gguf', 9 * 1024 ** 3)): require(value[name] == {'path': value[name]['path'], **file_identity(root / value[name]['path'], maximum)}, 'native_artifact_binding') diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 8312cf2..7444268 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -35,8 +35,8 @@ LARGE_CORE = '6a517b576baa17e7329661ee0476d1848081d114' MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') -# Native cleanup and source-matched Qwen3 rotary metadata verification. -NATIVE_CORE = '773d6377e5263c24a9263f7b97b5c9ae739b3f74' +# Native complete tensor validation is bounded and source-provenance checked. +NATIVE_CORE = 'f4cd8bd7ca310794dfc73cfcd096d6baedcf73bf' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index e7daebb..6207cb9 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-3.0-only """Inert native fixture bindings; no source fetch, build, library or model load.""" import hashlib +import copy import importlib.util import inspect import io @@ -16,7 +17,7 @@ import time from types import SimpleNamespace import unittest -from unittest.mock import patch +from unittest.mock import Mock, patch ROOT = Path(__file__).resolve().parents[1] @@ -36,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '773d6377e5263c24a9263f7b97b5c9ae739b3f74') + self.assertEqual(TRIAL.NATIVE_CORE, 'f4cd8bd7ca310794dfc73cfcd096d6baedcf73bf') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): @@ -701,8 +702,8 @@ def artifact(path, content): library = artifact(build_root / 'libvolparossa_llama_cpu.so', b'inert library bytes, never loaded') (backend / 'libvolparossa_llama_cpu.so').write_bytes((build_root / 'libvolparossa_llama_cpu.so').read_bytes()) wrapper = {name: artifact(wrappers / name, b'inert source ' + name.encode()) - for name in ('CMakeLists.txt', 'adapter.h', 'adapter.cpp')} - raw_build = dict(version=1, kind=KIND, source_commit=NATIVE.SOURCE, source_tree='b' * 40, + for name in ('CMakeLists.txt', 'adapter.h', 'adapter.cpp', 'bounded-tensor-validation.patch')} + raw_build = dict(version=2, kind=KIND, source_commit=NATIVE.SOURCE, source_tree='b' * 40, abi_version=1, jobs=2, cpu='avx2_fma_f16c', models_loaded=False, downloads=False, install=False, sanitizers=False, provisionable=True, quantization=False, library={'path': 'libvolparossa_llama_cpu.so', **library}, wrapper_sources=wrapper, @@ -719,9 +720,10 @@ def artifact(path, content): 'backend_sha256': hashlib.sha256((backend / 'backend.json').read_bytes()).hexdigest()}) # Core parsers/builders have separate tests; these doubles exercise only # this fixture consumer's artifact/provenance checks, not model evidence. + parser = SimpleNamespace(validate_manifest=Mock(), validate_build_provenance=Mock()) builder = SimpleNamespace(SOURCE=NATIVE.SOURCE, ORIGIN=NATIVE.ORIGIN, KIND=KIND, - ALLOWED_NEEDED={'libc.so.6'}, verify_source=lambda _path: 'b' * 40) - parser = SimpleNamespace(validate_manifest=lambda _value, _require: None) + ALLOWED_NEEDED={'libc.so.6'}, verify_source=lambda _path: 'b' * 40, + native=parser, verify_loader_compilation=Mock()) return base, core, report, builder, parser def test_exact_build_and_converted_manifest_bind_actual_inert_bytes(self): @@ -729,6 +731,13 @@ def test_exact_build_and_converted_manifest_bind_actual_inert_bytes(self): base, core, report, builder, parser = self.bundle_fixture(temp) with patch.object(NATIVE, 'module', return_value=builder): build = NATIVE.validate_build(core, base) + parser.validate_build_provenance.assert_called_once_with( + json.loads((base / 'llama-build/build.json').read_bytes()), NATIVE.require) + builder.verify_loader_compilation.assert_called_once_with( + base / 'llama-build', base / 'llama-build/loader-overlay/llama-model-loader.cpp') + self.assertEqual(set(build), {'kind', 'source_commit', 'source_tree', 'build_manifest_sha256', + 'library', 'wrapper_sources', 'jobs', 'nice', 'cpu', + 'quantization', 'models_loaded'}) with patch.object(NATIVE, 'module', return_value=parser): result = NATIVE.validate_provision(core, base, report, build) self.assertEqual(result['manifest_sha256'], report['native_backend']['backend_sha256']) @@ -745,6 +754,153 @@ def test_exact_build_and_converted_manifest_bind_actual_inert_bytes(self): with patch.object(NATIVE, 'module', return_value=builder), self.assertRaisesRegex(ValueError, 'native_wrapper_binding'): NATIVE.validate_build(core, base) + def test_core_provenance_and_loader_rejections_are_not_swallowed(self): + with tempfile.TemporaryDirectory() as temp: + base, core, _report, builder, parser = self.bundle_fixture(temp) + with patch.object(NATIVE, 'module', return_value=builder): + parser.validate_build_provenance.side_effect = ValueError('CORE_PATCH_REJECTED') + with self.assertRaisesRegex(ValueError, 'CORE_PATCH_REJECTED'): + NATIVE.validate_build(core, base) + builder.verify_loader_compilation.assert_not_called() + parser.validate_build_provenance.side_effect = None + builder.verify_loader_compilation.side_effect = ValueError('CORE_COMPILE_REJECTED') + with self.assertRaisesRegex(ValueError, 'CORE_COMPILE_REJECTED'): + NATIVE.validate_build(core, base) + + def test_patch_and_copied_build_tampering_fail_closed(self): + with tempfile.TemporaryDirectory() as temp: + base, core, report, builder, parser = self.bundle_fixture(temp) + with patch.object(NATIVE, 'module', return_value=builder): + build = NATIVE.validate_build(core, base) + (core / 'workers/volparossa-ml/native-cpu/bounded-tensor-validation.patch').write_bytes(b'changed') + with self.assertRaisesRegex(ValueError, 'native_wrapper_binding'): + NATIVE.validate_build(core, base) + copied = base / 'ml/native-backend/build.json' + original = copied.read_bytes() + with patch.object(NATIVE, 'module', return_value=parser): + copied.write_bytes(original + b' ') + with self.assertRaisesRegex(ValueError, 'native_copied_build_binding'): + NATIVE.validate_provision(core, base, report, build) + copied.write_bytes(original) + parser.validate_build_provenance.side_effect = ValueError('CORE_COPIED_PATCH_REJECTED') + with self.assertRaisesRegex(ValueError, 'CORE_COPIED_PATCH_REJECTED'): + NATIVE.validate_provision(core, base, report, build) + + def test_builder_import_uses_only_exact_sibling_and_restores_import_state(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + sibling = root / 'llama_cpu.py' + sibling.write_text('marker = "exact sibling"\n') + builder = root / 'build_llama_cpu.py' + builder.write_text('import llama_cpu as native\n') + original_path = list(sys.path) + for previous in (None, SimpleNamespace(marker='stale cached core')): + with patch.dict(sys.modules): + if previous is None: + sys.modules.pop('llama_cpu', None) + else: + sys.modules['llama_cpu'] = previous + value = NATIVE.module(builder, 'inert_exact_core_builder') + self.assertEqual(value.native.marker, 'exact sibling') + self.assertEqual(Path(value.native.__file__), sibling) + self.assertIsNot(value.native, previous) + self.assertEqual(sys.path, original_path) + if previous is None: + self.assertNotIn('llama_cpu', sys.modules) + else: + self.assertIs(sys.modules['llama_cpu'], previous) + # Failed imports must restore state too, without falling back to an + # existing module when the actual sibling is missing or a symlink. + stale = SimpleNamespace(marker='must never be selected') + with patch.dict(sys.modules, {'llama_cpu': stale}): + builder.write_text('import llama_cpu as native\nraise ValueError("inert import failed")\n') + with self.assertRaisesRegex(ValueError, 'inert import failed'): + NATIVE.module(builder, 'inert_failed_core_builder') + self.assertIs(sys.modules['llama_cpu'], stale) + builder.write_text('native = None\n') + with self.assertRaisesRegex(ValueError, 'native_builder_contract'): + NATIVE.module(builder, 'inert_rebound_core_builder') + self.assertIs(sys.modules['llama_cpu'], stale) + sibling.unlink() + with self.assertRaises(OSError): + NATIVE.module(builder, 'inert_missing_core_builder') + elsewhere = root / 'wrong_core.py' + elsewhere.write_text('marker = "wrong core"\n') + sibling.symlink_to(elsewhere) + with self.assertRaisesRegex(ValueError, 'native_module_path'): + NATIVE.module(builder, 'inert_symlink_core_builder') + self.assertIs(sys.modules['llama_cpu'], stale) + self.assertEqual(sys.path, original_path) + + def actual_core_builder(self): + core = Path(os.environ['VOLPAROSSA_NATIVE_CORE_TEST_SOURCE']).resolve(strict=True) + self.assertEqual(subprocess.check_output(['git', '-C', str(core), 'rev-parse', 'HEAD'], + text=True, timeout=10).strip(), TRIAL.NATIVE_CORE) + for name in ('llama_cpu.py', 'build_llama_cpu.py'): + relative = 'workers/volparossa-ml/' + name + self.assertEqual((core / relative).read_bytes(), subprocess.check_output( + ['git', '-C', str(core), 'show', TRIAL.NATIVE_CORE + ':' + relative], timeout=10)) + return NATIVE.module(core / 'workers/volparossa-ml/build_llama_cpu.py', 'actual_pinned_builder') + + @unittest.skipUnless(os.environ.get('VOLPAROSSA_NATIVE_CORE_TEST_SOURCE'), + 'requires explicitly retained exact core source; never downloads') + def test_actual_pinned_core_import_and_v2_provenance(self): + core = Path(os.environ['VOLPAROSSA_NATIVE_CORE_TEST_SOURCE']).resolve(strict=True) + stale = SimpleNamespace(marker='stale cached module') + original_path = list(sys.path) + with patch.dict(sys.modules, {'llama_cpu': stale}), patch('ctypes.CDLL') as library: + builder = self.actual_core_builder() + self.assertIs(sys.modules['llama_cpu'], stale) + self.assertEqual(sys.path, original_path) + self.assertEqual(Path(builder.native.__file__), core / 'workers/volparossa-ml/llama_cpu.py') + native = builder.native + value = dict(version=2, source_commit=native.SOURCE, source_tree=native.SOURCE_TREE, + source_overlay=native.loader_provenance(), loader_compile_source_verified=True, + upstream_original_unchanged=True) + native.validate_build_provenance(value, NATIVE.require) + for changes in ({'version': 1}, {'version': True}, {'source_tree': '0' * 40}, + {'source_commit': '0' * 40}, {'source_overlay': {}}, + {'loader_compile_source_verified': False}, {'upstream_original_unchanged': False}): + with self.subTest(changes=changes), self.assertRaisesRegex(ValueError, 'NATIVE_BUILD_PATCH_BINDING'): + native.validate_build_provenance(dict(value, **changes), NATIVE.require) + for field in ('patch_sha256', 'effective_sha256', 'complete_tensor_validation', + 'owner_poll_between_tensors', 'validation_execution_threads'): + changed = copy.deepcopy(value) + changed['source_overlay'][field] = None + with self.subTest(field=field), self.assertRaisesRegex(ValueError, 'NATIVE_BUILD_PATCH_BINDING'): + native.validate_build_provenance(changed, NATIVE.require) + library.assert_not_called() + + @unittest.skipUnless(os.environ.get('VOLPAROSSA_NATIVE_CORE_TEST_SOURCE') + and os.environ.get('VOLPAROSSA_NATIVE_LOADER_TEST_SOURCE'), + 'requires retained exact core and patched loader source; never builds') + def test_actual_pinned_core_compile_source_proof_rejects_tampering(self): + builder = self.actual_core_builder() + retained = Path(os.environ['VOLPAROSSA_NATIVE_LOADER_TEST_SOURCE']) + self.assertEqual(NATIVE.file_identity(retained, 1024 ** 2)['sha256'], + builder.native.LOADER_EFFECTIVE_SHA) + raw = retained.read_bytes() + with tempfile.TemporaryDirectory() as temp, patch('ctypes.CDLL') as library: + output = Path(temp) + (output / 'build').mkdir() + (output / 'loader-overlay').mkdir() + loader = output / 'loader-overlay/llama-model-loader.cpp' + loader.write_bytes(raw) + database = output / 'build/compile_commands.json' + exact = {'file': str(loader)} + database.write_text(json.dumps([exact])) + builder.verify_loader_compilation(output, loader) + for rows in ([exact, exact], [{'file': str(output / 'original/llama-model-loader.cpp')}], + [{'file': str(output / 'other.cpp')}]): + database.write_text(json.dumps(rows)) + with self.subTest(rows=rows), self.assertRaisesRegex(ValueError, 'NATIVE_COMPILED_LOADER'): + builder.verify_loader_compilation(output, loader) + database.write_text(json.dumps([exact])) + loader.write_bytes(raw + b'\n') + with self.assertRaisesRegex(ValueError, 'NATIVE_COMPILED_LOADER'): + builder.verify_loader_compilation(output, loader) + library.assert_not_called() + if __name__ == '__main__': unittest.main() From 2c667d450afb354ffd287938ff4092bd59a83972 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Wed, 7 Oct 2026 20:06:30 +0200 Subject: [PATCH 14/39] test(opencode): pin classified native tool JSON diagnostics --- docs/OPENCODE.md | 44 +++++++++++++++++++++++++---- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index abee79f..82ba7d3 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -723,7 +723,7 @@ stall's cause. Original artifact ZIP SHA-256: An explicitly selected `--inference-backend llama_cpp_bf16_v1` now prepares a different CPU executor for that same original 4B task. It pins core -`f4cd8bd7ca310794dfc73cfcd096d6baedcf73bf` and llama.cpp source +`bccd7f3ee613f4132a506e32b1f098806d81ec44` and llama.cpp source `7fe450e19305b828c199d602c23a8337aaa1f03b`. The default `torch` backend and both existing model profiles retain their previous core pins. Native selection is refused for the smaller profile; there is no silent backend fallback. @@ -867,10 +867,41 @@ remains version 1; the build-manifest version is a separate contract. Focused inert checks cover the source-pinned import, provenance, tampering and unchanged resource profile. A separate core source-only build passes its narrow -ABI and validation checks. Neither proves a full model load or successful coding -task on this candidate; a new separately authorized isolated trial is required. +ABI and validation checks. Neither proved a full model load or successful coding +task at that source-only checkpoint. The following actual trial remains failed. The model, original prompt, precision, deadlines and memory limits are unchanged. +The subsequent [trial 37654339995](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37654339995), +attempt 1 on Code `8b5d5b6a` / core `f4cd8bd7`, **fails** after native source +build, conversion and provisioning complete. One submitted request returns one +cleanup-confirmed incomplete model result (`invalid_output`), with no usable +turn, tool call, permission request or completed command. The provider records +`invalid_model_output` once; execution-failure, token-limit and wire-truncation +counters remain zero. Core observes `result_observed` once and +`conversation_native_json` twice: repeated validation of the same report, not +two model executions. The tagged tool body failed typed JSON deserialization; +the exact syntax/schema cause is unknown. No raw model output was exported. + +Core cgroup peak is 4,555,878,400 bytes within 11 GiB, with zero OOM kills and +swap. Task-memory and graceful-core-stop fields are absent, not passing or zero +observations. Reported owned units/process-group/private data and QEMU/scratch +cleanup pass. Normalized route/DNS observations match; raw IPv6 route hashes +differ, so byte-identical host state or a full firewall audit is not claimed. +Original artifact `11499080343` ZIP SHA-256: +`c7d3ed9150e8eb8e7d58a65ed46b0179c997a74f2191c621cf0109552278b6dd`; +job `112905525075` decoded log SHA-256: +`fd62b12408eee3e4bbf6c994a84a9cede4fd088806103525782cf428f1ff6610`. +All original failed runs and consumed one-shot receipts remain unchanged. + +The new exact core pin is a diagnostic-only candidate. After the unchanged +private report binding, it exports only Serde's closed `syntax`, `data`, `eof` +or `io` category, never parser messages, field names, positions or output +fragments. Historical generic JSON codes remain readable. This does not repair +or accept the rejected output, establish a unique prompt/template cause, or +prove another model execution. The wire result remains `invalid_output`; the +single-call contract, complete prompt/model, resources and deadlines are unchanged. +No model trial has run on this diagnostic follow-up. + Select the native backend consistently for source selection, packing and execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow exposes the same explicit choice. It must be registered and dispatched against @@ -894,8 +925,9 @@ unchanged defaults, measured resource limits and cleanup contracts. The actual build-shell selector now forwards the explicit backend choice too; an inert shell-prefix regression reproduces the previous mismatch before any profile, privileged operation or build. Source-only CI also covers stacked pull requests, -with unchanged checks and permissions. No actual -native model inference or successful coding loop has yet been proved. Conversion -and provisioning are demonstrated by the separate real trials above. +with unchanged checks and permissions. The real trials above now demonstrate +conversion, provisioning and one cleanup-confirmed incomplete native model result, +but no usable native turn or successful coding loop. The newer diagnostics remain +source-only evidence. The core's retained full upstream sanitizer failure on a disallowed quantized path also remains explicit; separate BF16/F32 checks do not erase that failure. diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 7444268..0bbec47 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native complete tensor validation is bounded and source-provenance checked. -NATIVE_CORE = 'f4cd8bd7ca310794dfc73cfcd096d6baedcf73bf' +NATIVE_CORE = 'bccd7f3ee613f4132a506e32b1f098806d81ec44' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 6207cb9..5d6e51b 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, 'f4cd8bd7ca310794dfc73cfcd096d6baedcf73bf') + self.assertEqual(TRIAL.NATIVE_CORE, 'bccd7f3ee613f4132a506e32b1f098806d81ec44') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 2b2687fec5376b6e49f8266dff71cc7935845e99 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Wed, 7 Oct 2026 20:13:51 +0200 Subject: [PATCH 15/39] test(opencode): track native candidate integrated with core main --- docs/OPENCODE.md | 9 ++++++++- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 82ba7d3..c16b13f 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -723,7 +723,7 @@ stall's cause. Original artifact ZIP SHA-256: An explicitly selected `--inference-backend llama_cpp_bf16_v1` now prepares a different CPU executor for that same original 4B task. It pins core -`bccd7f3ee613f4132a506e32b1f098806d81ec44` and llama.cpp source +`36d616fb6d79a36fe7c8ee95c4ad84bd56b561ae` and llama.cpp source `7fe450e19305b828c199d602c23a8337aaa1f03b`. The default `torch` backend and both existing model profiles retain their previous core pins. Native selection is refused for the smaller profile; there is no silent backend fallback. @@ -902,6 +902,13 @@ prove another model execution. The wire result remains `invalid_output`; the single-call contract, complete prompt/model, resources and deadlines are unchanged. No model trial has run on this diagnostic follow-up. +The exact pin includes main's independently verified public-filter snapshot +milestone. Its only merge conflict was two adjacent implementation-status +sections; both are retained. Native inference and diagnostics remain byte-for-byte +identical to `bccd7f3e`. This integration needs fresh exact-source CI; earlier +CodeQL results are not substituted for an absent Quality run on the conflicted +parent branch. + Select the native backend consistently for source selection, packing and execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow exposes the same explicit choice. It must be registered and dispatched against diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 0bbec47..8ec7ecd 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native complete tensor validation is bounded and source-provenance checked. -NATIVE_CORE = 'bccd7f3ee613f4132a506e32b1f098806d81ec44' +NATIVE_CORE = '36d616fb6d79a36fe7c8ee95c4ad84bd56b561ae' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 5d6e51b..221dd56 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, 'bccd7f3ee613f4132a506e32b1f098806d81ec44') + self.assertEqual(TRIAL.NATIVE_CORE, '36d616fb6d79a36fe7c8ee95c4ad84bd56b561ae') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 45510055bfe4f8ead010e403e701e5c710efe499 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:12:30 +0200 Subject: [PATCH 16/39] Pin scoped native weight verification and retain deadline evidence --- README.md | 7 ++++ docs/OPENCODE.md | 58 ++++++++++++++++++++++++++--- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2ac7c70..9ed991e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,13 @@ adapter, editor and lifecycle behavior. The complete model-driven OpenCode read/edit/test loop, native editor UI operation, protected peer execution and a finished immune-policy path remain unproved. +**Latest real native CPU trial:** the [4B run](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37671030306) +failed at the execution deadline during post-generation verification. The worker +reported 44 generated tokens internally, but no verified native turn, tool call, +edit or test completed. This is a different failure from the preceding JSON +rejection; neither proves a working coding loop. See the +[native CPU evidence](docs/OPENCODE.md#explicit-native-cpu-candidate). + **Real public peer proof:** a separate owner command successfully obtained a small single-file replacement from a real Qwen0.6B worker on another peer. After explicit edit and check approvals, the unchanged original tests and an diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index c16b13f..4c12bbc 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -900,15 +900,61 @@ fragments. Historical generic JSON codes remain readable. This does not repair or accept the rejected output, establish a unique prompt/template cause, or prove another model execution. The wire result remains `invalid_output`; the single-call contract, complete prompt/model, resources and deadlines are unchanged. -No model trial has run on this diagnostic follow-up. +No model trial had run at this diagnostic checkpoint; the actual follow-up is +recorded below. The exact pin includes main's independently verified public-filter snapshot milestone. Its only merge conflict was two adjacent implementation-status sections; both are retained. Native inference and diagnostics remain byte-for-byte -identical to `bccd7f3e`. This integration needs fresh exact-source CI; earlier -CodeQL results are not substituted for an absent Quality run on the conflicted +identical to `bccd7f3e`. This integration required fresh exact-source CI; earlier +CodeQL results were not substituted for an absent Quality run on the conflicted parent branch. +The diagnostic follow-up [trial 37671030306](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37671030306), +attempt 1 on Code `2b2687fec5376b6e49f8266dff71cc7935845e99` / core +`36d616fb6d79a36fe7c8ee95c4ad84bd56b561ae`, **fails at the execution +deadline**. Native source build, conversion and provisioning complete. Unlike the +previous JSON rejection, this request ends with core `compute_deadline` and one +provider `execution_budget_exceeded` error, with cleanup confirmed. The worker +records 44 generated tokens and an internal generation completion at 518,788 ms; +`verify_after/begin` follows at 519,120 ms. Its last state is paused under +`quiet_hold`, with 31 controls issued and acknowledged, the last being pause. +Those tokens never become a verified result delivered to the native runtime. + +The task ends after 603,331 ms without a completed native turn, observed tool +call, approved read/edit/test or completed command. The fixture is unchanged and +independent verification is not reached. Core cgroup peak is 9,466,974,208 bytes +within 11 GiB, with zero OOM kills and swap. These observations do not measure +total pause time or identify a unique cause of the overrun. Reported runtime, +project, guest-unit/private-data, QEMU/scratch and owned CI-host cleanup pass. +Normalized route/DNS observations match, but raw hashes differ; no byte-identical +host-state or complete firewall-audit claim follows. Raw model output remains +excluded. + +Original artifact `11505764017` ZIP SHA-256: +`935e115a86abf1da83fe0e3d68ebac90d37ecbe399c7dcc79f055ff26dc33e83`; +job `112962546447` original raw log, 68,786 bytes, SHA-256: +`369407954fff82728d2ff41d5d0e229c78726ea8ae487bf2112964138ca492d8`. +The original bytes are retained without decoding or newline normalization. This +failed run does not replace or relabel any earlier result or relax model, task, +integrity, resource or deadline checks. + +The next source-only pin is core +`9e9086ee873f7162f498e8647e6f6f438273e6ce` (tree +`ecda529c486e4f759f15d107b5e09a7710bcbfa6`). Native execution now retains the actual +complete initial source-shard measurement rather than reading the unused +conversion-source shards again after generation. The used GGUF is still fully +hashed before loading and after closing the native handle. Its required internal +`verification_scope` states this distinction; the core rejects missing, extra, +old or different scope. This is an explicit native verification-contract change, +not successful execution evidence. Ordinary PyTorch checks and the complete +model, task, resource, governor and deadline settings remain unchanged. + +The pin includes main's filter broker and four-validator TEST milestone; the +merge preserves both independent third-party provenance sections. A new actual +trial requires fresh exact-source CI and preflight. No new model trial has run +on this source correction, and the original failures above remain unchanged. + Select the native backend consistently for source selection, packing and execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow exposes the same explicit choice. It must be registered and dispatched against @@ -933,8 +979,8 @@ build-shell selector now forwards the explicit backend choice too; an inert shell-prefix regression reproduces the previous mismatch before any profile, privileged operation or build. Source-only CI also covers stacked pull requests, with unchanged checks and permissions. The real trials above now demonstrate -conversion, provisioning and one cleanup-confirmed incomplete native model result, -but no usable native turn or successful coding loop. The newer diagnostics remain -source-only evidence. +conversion, provisioning and one cleanup-confirmed incomplete native model result. +Neither that result nor the latest deadline during post-generation verification +proves a usable native turn or successful coding loop. The core's retained full upstream sanitizer failure on a disallowed quantized path also remains explicit; separate BF16/F32 checks do not erase that failure. diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 8ec7ecd..a1d21a2 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native complete tensor validation is bounded and source-provenance checked. -NATIVE_CORE = '36d616fb6d79a36fe7c8ee95c4ad84bd56b561ae' +NATIVE_CORE = '9e9086ee873f7162f498e8647e6f6f438273e6ce' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 221dd56..4da81b9 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '36d616fb6d79a36fe7c8ee95c4ad84bd56b561ae') + self.assertEqual(TRIAL.NATIVE_CORE, '9e9086ee873f7162f498e8647e6f6f438273e6ce') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 5c6248b2c648998b5e575033284087fe6849259d Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:32:04 +0200 Subject: [PATCH 17/39] feat(opencode): negotiate explicit native output constraints --- .github/workflows/source-checks.yml | 2 + docs/OPENCODE.md | 22 ++++ scripts/opencode_session.cjs | 18 ++- scripts/opencode_session.py | 19 ++- src/chat-completions-provider.cjs | 20 ++- src/opencode-runtime.cjs | 6 +- src/private-conversation.cjs | 41 ++++-- tests/opencode-runtime.test.cjs | 5 + tests/opencode-session.test.cjs | 21 ++- tests/output-constraint.test.cjs | 134 +++++++++++++++++++ tests/test_output_constraint_session.py | 165 ++++++++++++++++++++++++ 11 files changed, 423 insertions(+), 30 deletions(-) create mode 100644 tests/output-constraint.test.cjs create mode 100644 tests/test_output_constraint_session.py diff --git a/.github/workflows/source-checks.yml b/.github/workflows/source-checks.yml index 51dbfad..3584f35 100644 --- a/.github/workflows/source-checks.yml +++ b/.github/workflows/source-checks.yml @@ -26,6 +26,8 @@ jobs: run: python3 -B -m unittest discover -s tests -p 'test_build_codex_runtime.py' - name: Check OpenCode source-build and namespace contracts run: python3 -B -m unittest discover -s tests -p 'test_*opencode*.py' + - name: Check explicit output-constraint session selection + run: python3 -B -m unittest discover -s tests -p test_output_constraint_session.py # No dependency installation, runtime/model download, real inference, editor # installation or privileged service startup. These are source/protocol checks. diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 4c12bbc..8164c40 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -62,6 +62,28 @@ the original task budget. See [editor setup and check limitations](OWNER_VERIFIC ### Core-selected coding profiles +An experimental owner-selected `outputConstraint: "qwen_tagged_tool_json_v1"` +field may be added to `volparossaCode.openCodeRuntime`. It requires the matching +new core capability and native Qwen4B backend; an older or unsupported core is +refused before OpenCode starts. Omitting the field preserves the existing +session and request forms. Opening a workspace still starts nothing. + +Each provider connection negotiates the same capability. Requests with offered +tools carry the selector; requests without tools omit it. Ordinary answers +remain allowed, and tools still require their existing execution authority. +The core controls alias derivation and constrained generation; the application +accepts neither a caller-supplied grammar nor an unrequested result claim. There +is no generated-output repair, forced tool use, model substitution or larger +budget. This profile does not imply arbitrary JSON-schema validation or private +execution on other peers. + +76 targeted Node contract tests and ten inert Python session-selection tests +pass. They cover actual HTTP/Unix framing with synthetic core replies, legacy +compatibility, result binding, namespace command selection and startup refusal. +No model or OpenCode binary ran in those tests. Core native compilation and a +new pinned real OpenCode edit/test trial remain required; the earlier invalid +JSON trial is not considered fixed by these protocol checks. + - `qwen3-0.6b-v1`: the unchanged default; native template `qwen3-tools-nonthinking-v1`, model context 32,768 tokens. - `qwen3-4b-instruct-2507-v1`: the explicit larger profile; native template diff --git a/scripts/opencode_session.cjs b/scripts/opencode_session.cjs index 35c974e..51e70a7 100644 --- a/scripts/opencode_session.cjs +++ b/scripts/opencode_session.cjs @@ -9,7 +9,7 @@ const {randomBytes} = require('node:crypto'); const {setTimeout: delay} = require('node:timers/promises'); const {OpenCodeClient} = require('../src/opencode-client.cjs'); const {OpenCodeTask} = require('../src/opencode-task.cjs'); -const {PrivateConversation, capabilities} = require('../src/private-conversation.cjs'); +const {PrivateConversation, capabilities, OUTPUT_CONSTRAINT} = require('../src/private-conversation.cjs'); const {startChatCompletionsProvider} = require('../src/chat-completions-provider.cjs'); const {runtimeSettings, isCodingModel} = require('../src/opencode-config.cjs'); const {readFrames, writeFrame, taskFailure, record, validVerification} = require('../src/opencode-bridge.cjs'); @@ -44,7 +44,8 @@ async function port() { await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); return value; } -async function runSession({input, output, events = process}, hooks = {}) { +async function runSession({input, output, events = process, outputConstraint}, hooks = {}) { + if (outputConstraint !== undefined && outputConstraint !== OUTPUT_CONSTRAINT) return 1; const spawnServer = hooks.spawn ?? spawn; let provider, child, exited, client, task, running, closing = false, bad = false, requested = false; let seq = 0, verificationSeq = 0, pendingApproval, pendingVerification, finish; @@ -135,20 +136,22 @@ async function runSession({input, output, events = process}, hooks = {}) { for (const name of ['SIGTERM', 'SIGINT', 'SIGHUP']) events.once(name, broken); try { const caps = capabilities(await (hooks.preflight ?? (async () => { - const core = new PrivateConversation('/opt/core/compute.sock', {generationPolicyVersion: 1, executionErrorVersion: 1}); + const core = new PrivateConversation('/opt/core/compute.sock', {generationPolicyVersion: 1, executionErrorVersion: 1, + ...(outputConstraint !== undefined ? {outputConstraintVersion: 1} : {})}); try { return await core.connect(); } finally { core.close(); } - }))(), 1, 1); + }))(), 1, 1, outputConstraint !== undefined ? 1 : undefined); if (!isCodingModel(caps.model_profile) || !caps.native_tool_template || !caps.local_only || caps.quarantined || !caps.generation_policies.includes('greedy_v1')) throw Error('capabilities'); + if (outputConstraint !== undefined && !caps.output_constraints.includes(outputConstraint)) throw Error('capabilities'); // The owner's already selected core determines this session's one model. // Each provider request rechecks that identity; a core change cannot silently // switch the native task to another profile or provider. const model = caps.model_profile; if (closing) throw Error('closed'); provider = await (hooks.provider ?? startChatCompletionsProvider)({socketPath: '/opt/core/compute.sock', model, - diagnostics: true}); + diagnostics: true, ...(outputConstraint !== undefined ? {outputConstraint} : {})}); const password = randomBytes(32).toString('hex'); const cooperative = (hooks.cooperative ?? cooperativeMounted)(); const settings = runtimeSettings({baseUrl: provider.baseUrl, bearerToken: provider.bearerToken, password, cooperative, model}); @@ -204,7 +207,10 @@ async function runSession({input, output, events = process}, hooks = {}) { async function main() { if (process.platform !== 'linux' || process.getuid() === 0 || process.cwd() !== '/workspace' || Object.keys(process.env).some(key => !['PATH', 'LANG'].includes(key))) return 1; - return runSession({input: process.stdin, output: process.stdout}); + const args = process.argv.slice(2); + if (args.length && (args.length !== 2 || args[0] !== '--output-constraint' || args[1] !== OUTPUT_CONSTRAINT)) return 1; + return runSession({input: process.stdin, output: process.stdout, + ...(args.length ? {outputConstraint: OUTPUT_CONSTRAINT} : {})}); } if (require.main === module) main().then(code => { process.exitCode = code; }, () => { process.exitCode = 1; }); module.exports = {runSession, cooperativeMounted, prepareState}; diff --git a/scripts/opencode_session.py b/scripts/opencode_session.py index 4a238f6..6044657 100644 --- a/scripts/opencode_session.py +++ b/scripts/opencode_session.py @@ -14,6 +14,7 @@ PIN = 'aec0b9a6d8898f68f923aaf08b7306d931fd9d76' FIELDS = {'version', 'opencode', 'opencodeSha256', 'buildReport', 'node', 'nodeSha256', 'socketPath'} COOPERATIVE_FIELD = 'cooperativeSocketPath' +OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' SOURCES = ('private-compute.cjs', 'private-conversation.cjs', 'responses-provider.cjs', 'chat-completions-provider.cjs', 'opencode-config.cjs', 'opencode-client.cjs', 'opencode-task.cjs', 'opencode-bridge.cjs') @@ -73,8 +74,10 @@ def account_home(): def validate(config, workspace): - require(os.getuid() != 0 and type(config) is dict and set(config) in (FIELDS, FIELDS | {COOPERATIVE_FIELD}) + require(os.getuid() != 0 and type(config) is dict and FIELDS <= set(config) + and set(config) <= FIELDS | {COOPERATIVE_FIELD, 'outputConstraint'} and type(config['version']) is int and config['version'] == 1) + require('outputConstraint' not in config or config['outputConstraint'] == OUTPUT_CONSTRAINT) require(all(type(config[name]) is str and 0 < len(config[name]) <= 4096 and '\0' not in config[name] for name in set(config) - {'version'})) binary = file(config['opencode'], config['opencodeSha256'], True) @@ -101,10 +104,12 @@ def validate(config, workspace): authorities = (binary, node, report_path, ipc, ROOT) + ((cooperative,) if cooperative else ()) require(all(not item.is_relative_to(project) for item in authorities) and not project.is_relative_to(ROOT) and not project.is_relative_to(report_path.parent)) - return binary, node, ipc, project, home, cooperative + values = binary, node, ipc, project, home, cooperative + return values + (OUTPUT_CONSTRAINT,) if 'outputConstraint' in config else values -def command(binary, node, ipc, project, home, cooperative=None): +def command(binary, node, ipc, project, home, cooperative=None, output_constraint=None): + require(output_constraint is None or output_constraint == OUTPUT_CONSTRAINT) result = ['/usr/bin/bwrap', '--die-with-parent', '--new-session', '--unshare-user', '--uid', str(os.getuid()), '--gid', str(os.getgid()), '--unshare-net', '--unshare-pid', '--unshare-ipc', '--unshare-uts', '--cap-drop', 'ALL', '--ro-bind', '/usr', '/usr', @@ -127,11 +132,12 @@ def command(binary, node, ipc, project, home, cooperative=None): result += ['--ro-bind', str(ROOT / 'scripts' / name), '/opt/scripts/' + name] return result + ['--chdir', '/workspace', '--clearenv', '--setenv', 'PATH', '/usr/bin:/bin', '--setenv', 'LANG', 'C.UTF-8', '--', '/usr/bin/python3', '-B', '/opt/scripts/opencode_session.py', - '--inside', os.readlink('/proc/self/ns/net')] + '--inside', os.readlink('/proc/self/ns/net')] + ([OUTPUT_CONSTRAINT] if output_constraint is not None else []) def main(): - if len(sys.argv) == 3 and sys.argv[1] == '--inside': + if len(sys.argv) in (3, 4) and sys.argv[1] == '--inside': + require(len(sys.argv) == 3 or sys.argv[3] == OUTPUT_CONSTRAINT) require(os.getuid() != 0 and os.readlink('/proc/self/ns/net') != sys.argv[2] and {name for _, name in socket.if_nameindex()} <= {'lo'}) require(set(os.environ) <= {'PATH', 'LANG', 'LC_CTYPE', 'PWD'} @@ -140,7 +146,8 @@ def main(): caps = next(line.split()[1] for line in Path('/proc/self/status').read_text().splitlines() if line.startswith('CapEff:')) require(int(caps, 16) == 0) - os.execve('/opt/node', ['/opt/node', '/opt/scripts/opencode_session.cjs'], + selected = ['--output-constraint', OUTPUT_CONSTRAINT] if len(sys.argv) == 4 else [] + os.execve('/opt/node', ['/opt/node', '/opt/scripts/opencode_session.cjs', *selected], {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8'}) require(len(sys.argv) == 4 and sys.argv[1] == '--execute' and len(sys.argv[2]) <= 32768) values = validate(json.loads(sys.argv[2], object_pairs_hook=no_duplicates), sys.argv[3]) diff --git a/src/chat-completions-provider.cjs b/src/chat-completions-provider.cjs index 84fe361..c248641 100644 --- a/src/chat-completions-provider.cjs +++ b/src/chat-completions-provider.cjs @@ -9,7 +9,7 @@ const http = require('node:http'); const { randomBytes, timingSafeEqual } = require('node:crypto'); const { TextDecoder } = require('node:util'); const { PrivateConversation, validateConversation, expectedLimits, check, keys, text, - identifier, object, fail } = require('./private-conversation.cjs'); + identifier, object, fail, OUTPUT_CONSTRAINT } = require('./private-conversation.cjs'); const { parseJson } = require('./responses-provider.cjs'); const { PROVIDER_ERRORS, emptyProviderDiagnostic } = require('./opencode-bridge.cjs'); const { terminalCleanupConfirmed } = require('./private-compute.cjs'); @@ -66,7 +66,11 @@ function toolChoice(request, tools) { return choice; } -function toConversation(request, model, caps) { +function toConversation(request, model, caps, outputConstraint) { + check(outputConstraint === undefined || outputConstraint === OUTPUT_CONSTRAINT, 'unsupported_output_constraint'); + if (outputConstraint !== undefined) check(caps.output_constraint_version === 1 && + caps.output_constraints?.includes(outputConstraint) && model === 'qwen3-4b-instruct-2507-v1' && + caps.inference_backend === 'llama_cpp_bf16_v1', 'unsupported_output_constraint'); keys(request, ['model', 'messages'], ['stream', 'stream_options', 'tools', 'tool_choice', 'max_tokens', 'temperature', 'top_p', 'frequency_penalty', 'presence_penalty', 'stop', 'seed', 'response_format', 'user', 'reasoning_effort', 'verbosity', 'parallel_tool_calls', 'n', 'store']); @@ -146,7 +150,8 @@ function toConversation(request, model, caps) { } const conversation = { version: 1, visibility: 'private_local', instructions: instructions.length ? instructions.join('\n\n') : 'Answer the user; tool proposals require separate execution authority.', - history, tools, ...(greedy ? { generation_policy: 'greedy_v1' } : {}) }; + history, tools, ...(greedy ? { generation_policy: 'greedy_v1' } : {}), + ...(outputConstraint !== undefined && tools.length ? { output_constraint: outputConstraint } : {}) }; validateConversation(conversation, caps); return conversation; } @@ -211,8 +216,10 @@ async function readBody(request) { catch (error) { throw error.message?.startsWith('private_compute_') ? error : fail('invalid_json'); } } -async function startChatCompletionsProvider({ socketPath, model, diagnostics = false }) { +async function startChatCompletionsProvider({ socketPath, model, diagnostics = false, outputConstraint }) { expectedLimits(model); + check(outputConstraint === undefined || outputConstraint === OUTPUT_CONSTRAINT && + model === 'qwen3-4b-instruct-2507-v1', 'unsupported_output_constraint'); check(typeof diagnostics === 'boolean', 'diagnostic_scope'); new PrivateConversation(socketPath); // Path validation only; launch does not start compute. const bearerToken = randomBytes(32).toString('base64url'); @@ -241,7 +248,8 @@ async function startChatCompletionsProvider({ socketPath, model, diagnostics = f if (closing || active) { errorReply(response, 503, 'busy'); return; } if (Number(request.headers['content-length'] ?? 0) > HTTP_BYTES) { errorReply(response, 413, 'request_bound'); return; } const controller = new AbortController(); - const client = new PrivateConversation(socketPath, { generationPolicyVersion: 1, executionErrorVersion: 1 }); + const client = new PrivateConversation(socketPath, { generationPolicyVersion: 1, executionErrorVersion: 1, + ...(outputConstraint !== undefined ? { outputConstraintVersion: 1 } : {}) }); const owner = { controller, client, done: null }; active = owner; response.once('close', () => { if (!response.writableFinished) controller.abort(); }); @@ -251,7 +259,7 @@ async function startChatCompletionsProvider({ socketPath, model, diagnostics = f const requestBody = await readBody(request); if (controller.signal.aborted) throw fail('cancelled'); const caps = await client.connect(); - const conversation = toConversation(requestBody, model, caps); + const conversation = toConversation(requestBody, model, caps, outputConstraint); if (controller.signal.aborted) throw fail('cancelled'); observed.submitted++; if (diagnostics) count(summary, 'submitted'); diff --git a/src/opencode-runtime.cjs b/src/opencode-runtime.cjs index 4624187..a414e0e 100644 --- a/src/opencode-runtime.cjs +++ b/src/opencode-runtime.cjs @@ -3,13 +3,15 @@ const path = require('node:path'); const {spawn} = require('node:child_process'); const {MODEL, isCodingModel} = require('./opencode-config.cjs'); +const {OUTPUT_CONSTRAINT} = require('./private-conversation.cjs'); const {readFrames, writeFrame, record, isFailureCode, validProviderDiagnostic, validTaskDiagnostic, validVerification, validVerificationSummary} = require('./opencode-bridge.cjs'); const FIELDS = ['version', 'opencode', 'opencodeSha256', 'buildReport', 'node', 'nodeSha256', 'socketPath']; const fail = (code = 'runtime_failed') => Object.assign(Error('opencode_runtime_unavailable_or_cleanup_unconfirmed'), {code}); function configuration(value, workspace) { - if (!record(value) || value.version !== 1 || Object.keys(value).length !== FIELDS.length || - !FIELDS.every(key => Object.hasOwn(value, key))) throw fail(); + if (!record(value) || value.version !== 1 || !FIELDS.every(key => Object.hasOwn(value, key)) || + Object.keys(value).some(key => !FIELDS.includes(key) && key !== 'outputConstraint') || + Object.hasOwn(value, 'outputConstraint') && value.outputConstraint !== OUTPUT_CONSTRAINT) throw fail(); for (const key of FIELDS.slice(1)) { const item = value[key]; if (typeof item !== 'string' || !item || item.includes('\0') || Buffer.byteLength(item) > 4096 || diff --git a/src/private-conversation.cjs b/src/private-conversation.cjs index 29e197b..269ee6e 100644 --- a/src/private-conversation.cjs +++ b/src/private-conversation.cjs @@ -41,6 +41,7 @@ const NATIVE_PROFILES = Object.freeze({ 'qwen3-4b-instruct-2507-v1': {context: 262144, template: 'qwen3-tools-instruct-2507-v1'}, }); const nativeProfile = model => Object.hasOwn(NATIVE_PROFILES, model); +const OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1'; function expectedLimits(model) { check(Object.hasOwn(PROFILES, model), 'incompatible_capabilities'); const [prompt, output, bytes] = PROFILES[model]; @@ -63,14 +64,15 @@ function equalLimits(value, expected, optional = []) { keys(value, Object.keys(expected), optional); check(Object.entries(expected).every(([key, item]) => value[key] === item), 'incompatible_capabilities'); } -function capabilities(value, generationPolicyVersion, executionErrorVersion) { +function capabilities(value, generationPolicyVersion, executionErrorVersion, outputConstraintVersion) { check(object(value)); const generation = generationPolicyVersion === 1 ? ['generation_policy_version', 'generation_policies'] : []; const execution = executionErrorVersion === 1 ? ['execution_error_version'] : []; + const constraint = outputConstraintVersion === 1 ? ['output_constraint_version', 'output_constraints'] : []; const backend = ['inference_backend', 'required_generation_policy']; equalLimits(value, { ...expectedLimits(value.model_profile), execution_slots: 1, max_request_bytes: requestLimit(value.model_profile), max_response_bytes: 65536 }, - ['max_seconds', 'quarantined', ...generation, ...execution, ...backend]); + ['max_seconds', 'quarantined', ...generation, ...execution, ...constraint, ...backend]); // The owner-selected core may expose this one backend restriction, not a // frontend executor choice or independent proof of the returned backend. if (backend.some(key => Object.hasOwn(value, key))) { @@ -88,11 +90,19 @@ function capabilities(value, generationPolicyVersion, executionErrorVersion) { Object.freeze(value.generation_policies); } if (executionErrorVersion === 1) check(value.execution_error_version === 1, 'incompatible_capabilities'); + if (outputConstraintVersion === 1) { + check(value.output_constraint_version === 1 && Array.isArray(value.output_constraints) && + (value.output_constraints.length === 0 || value.output_constraints.length === 1 && + value.output_constraints[0] === OUTPUT_CONSTRAINT && + value.model_profile === 'qwen3-4b-instruct-2507-v1' && + value.inference_backend === 'llama_cpp_bf16_v1'), 'unsupported_output_constraint'); + Object.freeze(value.output_constraints); + } return Object.freeze(value); } function validateConversation(value, limits = expectedLimits('smollm2-360m-v1')) { - keys(value, ['version', 'visibility', 'instructions', 'history', 'tools'], ['generation_policy']); + keys(value, ['version', 'visibility', 'instructions', 'history', 'tools'], ['generation_policy', 'output_constraint']); check(value.version === 1 && value.visibility === 'private_local'); if (Object.hasOwn(value, 'generation_policy')) { check(value.generation_policy === 'greedy_v1' && nativeProfile(limits.model_profile) && @@ -102,6 +112,12 @@ function validateConversation(value, limits = expectedLimits('smollm2-360m-v1')) text(value.instructions, limits.max_instructions_bytes); check(Array.isArray(value.history) && value.history.length >= 1 && value.history.length <= limits.max_history_items && Array.isArray(value.tools) && value.tools.length <= limits.max_tools); + if (Object.hasOwn(value, 'output_constraint')) { + check(value.output_constraint === OUTPUT_CONSTRAINT && value.generation_policy === 'greedy_v1' && + limits.model_profile === 'qwen3-4b-instruct-2507-v1' && limits.inference_backend === 'llama_cpp_bf16_v1' && + limits.output_constraint_version === 1 && limits.output_constraints?.includes(OUTPUT_CONSTRAINT) && + value.tools.length >= 1 && value.tools.length <= 32, 'unsupported_output_constraint'); + } const tools = new Map(), seen = new Map(), open = new Set(); for (const tool of value.tools) { check(tool.type === 'function' || tool.type === 'custom'); @@ -147,11 +163,16 @@ function validateResult(value, caps, input) { keys(value, ['version', 'operation', 'model_profile', 'execution_complete', 'turn_complete', 'output', 'prompt_tokens', 'generated_tokens', 'limits', 'local_only', 'private_data_supported', 'tool_execution', 'distributed_execution_claimed', 'private_training_claimed', 'model_answer_correctness_proven', 'cleanup'], - Object.hasOwn(input, 'generation_policy') ? ['generation_policy'] : []); + [...(Object.hasOwn(input, 'generation_policy') ? ['generation_policy'] : []), + ...(Object.hasOwn(input, 'output_constraint') ? ['output_constraint'] : [])]); if (Object.hasOwn(input, 'generation_policy')) { validateConversation(input, caps); check(value.generation_policy === input.generation_policy, 'generation_policy_mismatch'); } + if (Object.hasOwn(input, 'output_constraint')) { + validateConversation(input, caps); + check(value.output_constraint === input.output_constraint, 'output_constraint_mismatch'); + } keys(value.cleanup, ['complete', 'retained_input', 'retained_report']); check(value.cleanup.complete === true && value.cleanup.retained_input === false && value.cleanup.retained_report === false, 'cleanup_unconfirmed'); @@ -180,12 +201,14 @@ function validateResult(value, caps, input) { } class PrivateConversation extends PrivateCompute { - constructor(socketPath, { generationPolicyVersion, executionErrorVersion } = {}) { + constructor(socketPath, { generationPolicyVersion, executionErrorVersion, outputConstraintVersion } = {}) { super(socketPath); check(generationPolicyVersion === undefined || generationPolicyVersion === 1, 'unsupported_generation_policy'); this.generationPolicyVersion = generationPolicyVersion; check(executionErrorVersion === undefined || executionErrorVersion === 1, 'unsupported_execution_error_version'); this.executionErrorVersion = executionErrorVersion; + check(outputConstraintVersion === undefined || outputConstraintVersion === 1, 'unsupported_output_constraint_version'); + this.outputConstraintVersion = outputConstraintVersion; } // These hooks reuse only the already-tested transport. Q&A callers and bytes // remain unchanged; this instance cannot silently use the old handshake. @@ -194,7 +217,8 @@ class PrivateConversation extends PrivateCompute { // the legacy Q&A class and its 32KiB framing remain byte-for-byte unchanged. if (operation.type === 'capabilities') operation = { type: 'conversation_capabilities', ...(this.generationPolicyVersion === 1 ? { generation_policy_version: 1 } : {}), - ...(this.executionErrorVersion === 1 ? { execution_error_version: 1 } : {}) }; + ...(this.executionErrorVersion === 1 ? { execution_error_version: 1 } : {}), + ...(this.outputConstraintVersion === 1 ? { output_constraint_version: 1 } : {}) }; try { const body = Buffer.from(JSON.stringify({ version: 1, id, operation })); check(body.length > 0 && body.length <= (this.caps?.max_request_bytes ?? 32768), 'request_bound'); @@ -234,7 +258,8 @@ class PrivateConversation extends PrivateCompute { if (message.event === 'conversation_capabilities') { keys(message, ['version', 'id', 'event', 'capabilities']); check(this.state === 'connecting' && message.id === this.handshake?.id, 'invalid_response'); - this.caps = capabilities(message.capabilities, this.generationPolicyVersion, this.executionErrorVersion); + this.caps = capabilities(message.capabilities, this.generationPolicyVersion, this.executionErrorVersion, + this.outputConstraintVersion); this.state = 'open'; clearTimeout(this.handshake.timer); this.handshake.resolve(this.caps); @@ -249,4 +274,4 @@ class PrivateConversation extends PrivateCompute { } module.exports = { PrivateConversation, validateConversation, validateResult, expectedLimits, requestLimit, capabilities, - check, keys, text, identifier, object, fail }; + OUTPUT_CONSTRAINT, check, keys, text, identifier, object, fail }; diff --git a/tests/opencode-runtime.test.cjs b/tests/opencode-runtime.test.cjs index 1607dd4..185063c 100644 --- a/tests/opencode-runtime.test.cjs +++ b/tests/opencode-runtime.test.cjs @@ -43,6 +43,11 @@ test('OpenCode runtime config accepts only exact explicit pinned inputs', () => const config = {version: 1, opencode: '/fixture/opencode', opencodeSha256: 'a'.repeat(64), buildReport: '/fixture/BUILD_REPORT.json', node: '/fixture/node', nodeSha256: 'b'.repeat(64), socketPath: '/fixture/private/core.sock'}; assert.deepEqual(configuration(config, '/fixture/project'), config); + const constrained = {...config, outputConstraint: 'qwen_tagged_tool_json_v1'}; + assert.deepEqual(configuration(constrained, '/fixture/project'), constrained); + for (const outputConstraint of [null, undefined, false, 'grammar']) { + assert.throws(() => configuration({...config, outputConstraint}, '/fixture/project'), /opencode_runtime/); + } for (const changed of [{...config, appServer: '/fixture/codex'}, {...config, opencodeSha256: 'bad'}, {...config, socketPath: 'relative'}, {...config, version: 2}]) { assert.throws(() => configuration(changed, '/fixture/project'), /opencode_runtime/); diff --git a/tests/opencode-session.test.cjs b/tests/opencode-session.test.cjs index f0fb074..a16325f 100644 --- a/tests/opencode-session.test.cjs +++ b/tests/opencode-session.test.cjs @@ -21,7 +21,7 @@ function fixture(Task, receive, options = {}) { const hooks = {Task, preflight: async () => options.capabilities ?? {...caps(options.model ?? DEFAULT_MODEL), generation_policy_version: 1, generation_policies: ['greedy_v1'], execution_error_version: 1}, - provider: async value => { binding.provider = value.model; return provider; }, + provider: async value => { binding.provider = value.model; binding.outputConstraint = value.outputConstraint; return provider; }, prepare(cooperative) { assert.equal(cooperative, options.cooperative ?? false); }, cooperative: () => options.cooperative ?? false, port: async () => 1235, approvalMs: 15, spawn(binary, args, config) { @@ -48,9 +48,26 @@ function fixture(Task, receive, options = {}) { } else receive(value, input); }, () => assert.fail('owner frame')); - return {observed, input, binding, done: runSession({input, output, events}, hooks).finally(unbind)}; + return {observed, input, binding, done: runSession({input, output, events, + outputConstraint: options.outputConstraint}, hooks).finally(unbind)}; } const result = {text: 'Fixture only', commands: 0, nativeTurnCompleted: true, taskVerified: false}; +test('explicit constraint is negotiated before runtime startup and bound to the provider', async () => { + const selected = 'qwen_tagged_tool_json_v1', model = 'qwen3-4b-instruct-2507-v1'; + class Task { async run() { return result; } } + const capabilities = {...caps(model), generation_policy_version: 1, generation_policies: ['greedy_v1'], + execution_error_version: 1, inference_backend: 'llama_cpp_bf16_v1', required_generation_policy: 'greedy_v1', + output_constraint_version: 1, output_constraints: [selected]}; + const f = fixture(Task, (_value, input) => input.end(), {model, capabilities, outputConstraint: selected}); + assert.equal(await f.done, 0); + assert.equal(f.binding.outputConstraint, selected); + for (const output_constraints of [[], undefined]) { + const rejected = fixture(Task, (_value, input) => input.end(), {model, outputConstraint: selected, + capabilities: {...capabilities, output_constraints}}); + assert.equal(await rejected.done, 1); + assert.equal(rejected.binding.provider, undefined); + } +}); test('owner connects pinned runtime, forwards exact approvals and waits for core/process cleanup', async () => { class Task { constructor(_client, approve) { this.approve = approve; } diff --git a/tests/output-constraint.test.cjs b/tests/output-constraint.test.cjs new file mode 100644 index 0000000..8b7836e --- /dev/null +++ b/tests/output-constraint.test.cjs @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Real framing with synthetic core replies; no inference or OpenCode proof. +'use strict'; +const assert = require('node:assert/strict'); +const http = require('node:http'); +const {test} = require('node:test'); +const {PrivateConversation, validateConversation, OUTPUT_CONSTRAINT: PROFILE} = require('../src/private-conversation.cjs'); +const {toConversation, startChatCompletionsProvider} = require('../src/chat-completions-provider.cjs'); +const {caps, input, result, reply, fixture} = require('./conversation-fixture.cjs'); +const MODEL = 'qwen3-4b-instruct-2507-v1'; +const OPTIONS = {generationPolicyVersion: 1, executionErrorVersion: 1, outputConstraintVersion: 1}; +const tool = {type: 'function', name: 'read', namespace: null, description: 'Synthetic read.', + parameters: {type: 'object'}}; +const advertised = () => ({...caps(MODEL), generation_policy_version: 1, generation_policies: ['greedy_v1'], + execution_error_version: 1, inference_backend: 'llama_cpp_bf16_v1', required_generation_policy: 'greedy_v1', + output_constraint_version: 1, output_constraints: [PROFILE]}); +const conversation = () => ({...input([tool]), generation_policy: 'greedy_v1', output_constraint: PROFILE}); +const answer = output => ({...result(output, MODEL), generation_policy: 'greedy_v1', output_constraint: PROFILE}); +const request = () => ({model: MODEL, temperature: 0, max_tokens: 1024, + messages: [{role: 'user', content: 'Use the synthetic example.'}], + tools: [{type: 'function', function: {name: tool.name, description: tool.description, parameters: tool.parameters}}]}); + +test('constraint negotiation is additive and binds each result to the requested profile', async t => { + for (const output of [undefined, {type: 'function_call', namespace: null, name: 'read', call_id: 'c1', arguments: {}}]) { + const original = answer(output); + const f = await fixture(t, (socket, message) => { + reply(socket, message, 'admitted'); reply(socket, message, 'result', {result: original}); + }, advertised(), OPTIONS); + assert.deepEqual(await f.client.connect(), advertised()); + assert.deepEqual(await f.client.submit(conversation()), original); + assert.deepEqual(f.requests[0].operation, {type: 'conversation_capabilities', generation_policy_version: 1, + execution_error_version: 1, output_constraint_version: 1}); + assert.deepEqual(f.requests[1].operation.conversation, conversation()); + assert.equal(Object.hasOwn(f.requests[1].operation.conversation, 'output_constraint_tools'), false); + assert.ok(Object.isFrozen(f.client.caps.output_constraints)); + } +}); + +test('unknown, partial or unsolicited capabilities cannot enable constrained execution', async t => { + for (const value of [null, true, 0, 2, '1']) { + assert.throws(() => new PrivateConversation('/fixture/core.sock', {outputConstraintVersion: value}), + /unsupported_output_constraint_version/); + } + for (const change of [{output_constraint_version: undefined}, {output_constraint_version: null}, + {output_constraint_version: 2}, {output_constraints: undefined}, {output_constraints: null}, + {output_constraints: [PROFILE, PROFILE]}, {output_constraints: ['arbitrary-grammar']}, + {inference_backend: undefined, required_generation_policy: undefined}, + {...caps('qwen3-0.6b-v1')}, {grammar: 'user-controlled'}]) { + const f = await fixture(t, () => assert.fail('must not submit'), {...advertised(), ...change}, OPTIONS); + await assert.rejects(f.client.connect()); + assert.equal(f.requests.length, 1); + } + const unsolicited = await fixture(t, () => assert.fail('must not submit'), advertised(), + {generationPolicyVersion: 1, executionErrorVersion: 1}); + await assert.rejects(unsolicited.client.connect()); +}); + +test('negotiated empty capabilities permit legacy input but not a constrained request', async t => { + const f = await fixture(t, () => assert.fail('must not submit'), {...advertised(), output_constraints: []}, OPTIONS); + await f.client.connect(); + validateConversation({...input(), generation_policy: 'greedy_v1'}, f.client.caps); + await assert.rejects(f.client.submit(conversation()), {code: 'unsupported_output_constraint'}); + assert.equal(f.requests.length, 1); +}); + +test('request profile is closed, requires greedy and real tools, and excludes caller aliases', () => { + for (const change of [{output_constraint: null}, {output_constraint: 'grammar'}, {output_constraint: true}, + {generation_policy: undefined}, {tools: []}, {output_constraint_tools: ['vp_0']}, + {tools: Array.from({length: 33}, (_, n) => ({...tool, name: 't' + n}))}]) { + assert.throws(() => validateConversation({...conversation(), ...change}, advertised())); + } + for (const count of [1, 32]) validateConversation({...conversation(), + tools: Array.from({length: count}, (_, n) => ({...tool, name: 't' + n}))}, advertised()); +}); + +test('missing, mismatched and unsolicited result constraints are rejected without output repair', async t => { + for (const value of [undefined, null, 'other', false]) { + const original = answer(); + if (value === undefined) delete original.output_constraint; else original.output_constraint = value; + const f = await fixture(t, (socket, message) => { + reply(socket, message, 'admitted'); reply(socket, message, 'result', {result: original}); + }, advertised(), OPTIONS); + await f.client.connect(); + await assert.rejects(f.client.submit(conversation()), {code: 'output_constraint_mismatch'}); + } + const f = await fixture(t, (socket, message) => { + reply(socket, message, 'admitted'); reply(socket, message, 'result', {result: answer()}); + }, advertised(), OPTIONS); + await f.client.connect(); + const legacy = conversation(); delete legacy.output_constraint; + await assert.rejects(f.client.submit(legacy)); +}); + +test('provider uses an explicit profile, never implicitly transforms legacy or no-tool input', () => { + const original = toConversation(request(), MODEL, advertised()); + assert.equal(Object.hasOwn(original, 'output_constraint'), false); + assert.deepEqual(toConversation(request(), MODEL, advertised(), PROFILE), {...original, output_constraint: PROFILE}); + const noTools = request(); delete noTools.tools; + assert.deepEqual(toConversation(noTools, MODEL, advertised(), PROFILE), toConversation(noTools, MODEL, advertised())); + assert.throws(() => toConversation(request(), MODEL, {...advertised(), output_constraints: []}, PROFILE), + /unsupported_output_constraint/); + assert.throws(() => toConversation(request(), MODEL, advertised(), null), /unsupported_output_constraint/); + const sampled = request(); delete sampled.temperature; + assert.throws(() => toConversation(sampled, MODEL, advertised(), PROFILE), /unsupported_output_constraint/); +}); + +function send(provider, body) { + return new Promise((resolve, reject) => { + const bytes = JSON.stringify(body); + const req = http.request(provider.baseUrl + '/chat/completions', {method: 'POST', headers: { + 'content-type': 'application/json', authorization: `Bearer ${provider.bearerToken}`, + 'content-length': Buffer.byteLength(bytes)}}, response => { + let raw = ''; response.setEncoding('utf8'); response.on('data', part => { raw += part; }); + response.on('end', () => resolve({status: response.statusCode, body: JSON.parse(raw)})); + }); + req.on('error', reject); req.end(bytes); + }); +} + +test('explicit HTTP provider negotiates on its own connection and preserves plain answers', async t => { + const f = await fixture(t, (socket, message) => { + assert.equal(message.operation.conversation.output_constraint, PROFILE); + reply(socket, message, 'admitted'); reply(socket, message, 'result', {result: answer()}); + }, advertised()); + const provider = await startChatCompletionsProvider({socketPath: f.socketPath, model: MODEL, outputConstraint: PROFILE}); + try { + const received = await send(provider, request()); + assert.equal(received.status, 200); + assert.equal(received.body.choices[0].message.content, answer().output.text); + assert.equal(received.body.choices[0].finish_reason, 'stop'); + assert.equal(f.requests[0].operation.output_constraint_version, 1); + assert.deepEqual(provider.observations, {submitted: 1, completed: 1, incomplete: 0, cleanup_confirmed: 1}); + } finally { await provider.close(); } +}); diff --git a/tests/test_output_constraint_session.py b/tests/test_output_constraint_session.py new file mode 100644 index 0000000..905faa7 --- /dev/null +++ b/tests/test_output_constraint_session.py @@ -0,0 +1,165 @@ +# SPDX-License-Identifier: GPL-3.0-only +"""Session argument contracts only; no namespace, executable, socket or model runs.""" +from contextlib import contextmanager +import importlib.util +import json +from pathlib import Path +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location('output_constraint_session', ROOT / 'scripts/opencode_session.py') +SESSION = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(SESSION) +PROFILE = 'qwen_tagged_tool_json_v1' +HASH = 'a' * 64 +VALUES = (Path('/inert/runtime/opencode'), Path('/inert/runtime/node'), + Path('/inert/private/compute.sock'), Path('/inert/project'), Path('/home/fixture'), None) +PROXY = Path('/inert/cooperative/proxy.sock') + + +def configuration(): + return {'version': 1, 'opencode': str(VALUES[0]), 'opencodeSha256': HASH, + 'node': str(VALUES[1]), 'nodeSha256': HASH, + 'buildReport': '/inert/runtime/build.json', 'socketPath': str(VALUES[2])} + + +@contextmanager +def validation_inputs(): + # Existing suites exercise real path/owner/socket safety. Stub those inputs + # here to test the exact new shape/tuple without opening any files or sockets. + pin = {'bun_lock_sha256': HASH, 'local_patch': 'inert.patch', 'tag': 'v1.0'} + report = {'version': 1, 'source_commit': SESSION.PIN, 'source_build': True, + 'lock_sha256': HASH, 'patch_sha256': HASH, + 'binary_sha256': HASH, 'runtime_version': '1.0'} + with patch.object(SESSION.os, 'getuid', return_value=1234), \ + patch.object(SESSION, 'file', side_effect=lambda value, *_: Path(value)), \ + patch.object(SESSION, 'owned', side_effect=lambda value, _: Path(value)), \ + patch.object(SESSION, 'private_socket', side_effect=Path), \ + patch.object(SESSION, 'account_home', return_value=VALUES[4]), \ + patch.object(SESSION, 'digest', return_value=HASH), \ + patch.object(Path, 'stat', return_value=SimpleNamespace(st_size=128)), \ + patch.object(Path, 'read_text', side_effect=[json.dumps(report), json.dumps(pin)]), \ + patch.object(Path, 'is_mount', return_value=False), \ + patch.object(SESSION.os, 'access', return_value=True): + yield + + +class Executed(Exception): + """Stop immediately at the mocked exec boundary; nothing is executed.""" + + +class OutputConstraintSessionTests(unittest.TestCase): + def test_legacy_validation_tuple_is_unchanged(self): + with validation_inputs(): + self.assertEqual(SESSION.validate(configuration(), str(VALUES[3])), VALUES) + + def test_selected_validation_only_appends_profile(self): + with validation_inputs(): + self.assertEqual(SESSION.validate(configuration() | {'outputConstraint': PROFILE}, + str(VALUES[3])), VALUES + (PROFILE,)) + + def test_cooperative_position_is_preserved_with_or_without_selection(self): + base = VALUES[:-1] + (PROXY,) + for selected in (False, True): + config = configuration() | {'cooperativeSocketPath': str(PROXY)} + if selected: + config['outputConstraint'] = PROFILE + with self.subTest(selected=selected), validation_inputs(): + self.assertEqual(SESSION.validate(config, str(VALUES[3])), + base + ((PROFILE,) if selected else ())) + + def test_unknown_null_or_nonstring_selection_is_rejected_before_artifacts(self): + for invalid in (None, False, True, 1, '', 'greedy_v1', 'arbitrary-grammar', + PROFILE + '\0', [PROFILE], {'profile': PROFILE}): + with self.subTest(value=invalid), patch.object(SESSION.os, 'getuid', return_value=1234), \ + patch.object(SESSION, 'file') as artifact: + with self.assertRaises(ValueError): + SESSION.validate(configuration() | {'outputConstraint': invalid}, str(VALUES[3])) + artifact.assert_not_called() + + def test_arbitrary_grammar_or_alias_fields_are_not_config_authority(self): + for extra in ({'grammar': 'root ::= anything'}, {'outputConstraintTools': ['vp_0']}, + {'output_constraint': PROFILE}): + with self.subTest(extra=extra), patch.object(SESSION.os, 'getuid', return_value=1234), \ + patch.object(SESSION, 'file') as artifact: + with self.assertRaises(ValueError): + SESSION.validate(configuration() | {'outputConstraint': PROFILE} | extra, str(VALUES[3])) + artifact.assert_not_called() + + def test_command_selection_only_appends_inside_argument(self): + for cooperative in (None, PROXY): + values = VALUES[:-1] + (cooperative,) + with self.subTest(cooperative=cooperative), \ + patch.object(SESSION.os, 'readlink', return_value='net:[inert-parent]'): + legacy = SESSION.command(*values) + selected = SESSION.command(*values, PROFILE) + self.assertEqual(selected, legacy + [PROFILE]) + self.assertEqual(legacy[-2:], ['--inside', 'net:[inert-parent]']) + self.assertNotIn(PROFILE, legacy) + self.assertNotIn('--output-constraint', selected) + self.assertEqual(selected.count('--bind'), 1) + self.assertIn('--unshare-net', selected) + self.assertIn('--clearenv', selected) + self.assertEqual('/opt/core/cooperative.sock' in selected, cooperative is not None) + + def test_command_unknown_selection_cannot_reach_namespace_observation(self): + with patch.object(SESSION.os, 'readlink') as namespace: + with self.assertRaises(ValueError): + SESSION.command(*VALUES, 'unknown') + namespace.assert_not_called() + + def test_inside_arguments_preserve_old_node_argv_and_add_only_selected_flag(self): + for selected in (False, True): + argv = ['session.py', '--inside', 'net:[inert-parent]'] + ([PROFILE] if selected else []) + with self.subTest(selected=selected), patch.object(SESSION.sys, 'argv', argv), \ + patch.object(SESSION.os, 'getuid', return_value=1234), \ + patch.object(SESSION.os, 'readlink', return_value='net:[inert-child]'), \ + patch.object(SESSION.socket, 'if_nameindex', return_value=[(1, 'lo')]), \ + patch.dict(SESSION.os.environ, {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8'}, clear=True), \ + patch.object(SESSION.pwd, 'getpwuid', return_value=SimpleNamespace(pw_dir='/home/fixture')), \ + patch.object(Path, 'iterdir', return_value=iter(())), \ + patch.object(Path, 'read_text', return_value='CapEff:\t0000000000000000\n'), \ + patch.object(SESSION.os, 'execve', side_effect=Executed) as execute: + with self.assertRaises(Executed): + SESSION.main() + expected = ['/opt/node', '/opt/scripts/opencode_session.cjs'] + if selected: + expected += ['--output-constraint', PROFILE] + execute.assert_called_once_with('/opt/node', expected, + {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8'}) + + def test_inside_unknown_or_extra_argument_never_executes(self): + for extra in (['unknown'], [''], [PROFILE, 'extra']): + with self.subTest(extra=extra), \ + patch.object(SESSION.sys, 'argv', ['session.py', '--inside', 'net:[inert-parent]', *extra]), \ + patch.object(SESSION.os, 'execve') as execute: + with self.assertRaises(ValueError): + SESSION.main() + execute.assert_not_called() + + def test_execute_preserves_validated_legacy_cooperative_and_selected_tuples(self): + for cooperative in (None, PROXY): + for selected in (False, True): + values = VALUES[:-1] + (cooperative,) + ((PROFILE,) if selected else ()) + config = configuration() + if cooperative is not None: + config['cooperativeSocketPath'] = str(cooperative) + if selected: + config['outputConstraint'] = PROFILE + with self.subTest(cooperative=cooperative, selected=selected), \ + patch.object(SESSION.sys, 'argv', ['session.py', '--execute', json.dumps(config), str(VALUES[3])]), \ + patch.object(SESSION, 'validate', return_value=values) as validate, \ + patch.object(SESSION, 'command', return_value=['inert-bwrap-argv']) as command, \ + patch.object(SESSION.os, 'execve', side_effect=Executed) as execute: + with self.assertRaises(Executed): + SESSION.main() + validate.assert_called_once_with(config, str(VALUES[3])) + command.assert_called_once_with(*values) + execute.assert_called_once_with('/usr/bin/bwrap', ['inert-bwrap-argv'], + {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8'}) + + +if __name__ == '__main__': + unittest.main() From 689c7dd22e6cdac5fb26a3fe0a193209302745fe Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:48:59 +0200 Subject: [PATCH 18/39] test(code): bind native OpenCode trial to output constraint capability --- docs/OPENCODE.md | 16 ++++- scripts/opencode_native_cpu.py | 36 +++++++++- scripts/smoke_opencode_inference.cjs | 37 ++++++++-- scripts/smoke_opencode_inference.py | 32 +++++++-- tests/smoke-opencode-inference.test.cjs | 35 +++++++++- tests/test_opencode_ci.py | 7 ++ tests/test_opencode_native_cpu.py | 90 ++++++++++++++++++++++--- tests/test_smoke_opencode_inference.py | 40 +++++++++++ 8 files changed, 264 insertions(+), 29 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 8164c40..03468e7 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -80,9 +80,19 @@ execution on other peers. 76 targeted Node contract tests and ten inert Python session-selection tests pass. They cover actual HTTP/Unix framing with synthetic core replies, legacy compatibility, result binding, namespace command selection and startup refusal. -No model or OpenCode binary ran in those tests. Core native compilation and a -new pinned real OpenCode edit/test trial remain required; the earlier invalid -JSON trial is not considered fixed by these protocol checks. +No model or OpenCode binary ran in those tests. The core's source-only normal +build now passes all five native check targets; ABI, loader, BF16/F32 and +output-constraint checks also pass under ASan/UBSan. The wider upstream tensor +test still fails on its previously recorded Q1_0/Q8_0 alignment error. See +[the original source-only run](https://github.com/VOLPAROSSA/volparossa/actions/runs/38671551374). +The earlier invalid JSON trial is not considered fixed by these contract checks. + +The next native trial is pinned to core +`9d044921f7e684737f9b10868429de42bc5416c9`, with matching build v3/backend v2 +provenance and an explicit selector passed into the actual OpenCode runtime. +The native guest and outer verifier require task receipt v2 with that selector; +legacy Torch arguments and task receipt v1 remain unchanged. Source checks still +gate the next real edit/test trial; no new model run has been dispatched. - `qwen3-0.6b-v1`: the unchanged default; native template `qwen3-tools-nonthinking-v1`, model context 32,768 tokens. diff --git a/scripts/opencode_native_cpu.py b/scripts/opencode_native_cpu.py index 0a45963..0347696 100644 --- a/scripts/opencode_native_cpu.py +++ b/scripts/opencode_native_cpu.py @@ -23,6 +23,9 @@ SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' ORIGIN = 'https://github.com/ggml-org/llama.cpp.git' KIND = 'llama_cpp_bf16_v1' +OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' +LEGACY_WRAPPERS = ('CMakeLists.txt', 'adapter.h', 'adapter.cpp', 'bounded-tensor-validation.patch') +CONSTRAINED_WRAPPERS = LEGACY_WRAPPERS + ('output-constraint.h', 'output-constraint.cpp') MODEL = 'qwen3-4b-instruct-2507-v1' BASE = Path('/home/vpci/opencode-trial') CORE = Path('/home/vpci/source') @@ -141,6 +144,10 @@ def validate_build(core, base): tree = builder.verify_source(base / 'llama-source') value, report_hash = document(base / 'llama-build/build.json', 65536) builder.native.validate_build_provenance(value, require) + require(type(value.get('version')) is int and value['version'] in (2, 3), 'native_build_version') + constrained = value['version'] == 3 + require(value.get('output_constraints') == [OUTPUT_CONSTRAINT] if constrained + else 'output_constraints' not in value, 'native_build_constraint') require(value.get('kind') == KIND and value.get('source_commit') == SOURCE and value.get('source_tree') == tree and value.get('abi_version') == 1 and value.get('jobs') == 2 and value.get('cpu') == 'avx2_fma_f16c' @@ -151,16 +158,19 @@ def validate_build(core, base): library = file_identity(base / 'llama-build/libvolparossa_llama_cpu.so', 128 * 1024 ** 2) require(value.get('library') == {'path': 'libvolparossa_llama_cpu.so', **library}, 'native_library_binding') wrapper = {name: file_identity(core / 'workers/volparossa-ml/native-cpu' / name, 1024 ** 2) - for name in ('CMakeLists.txt', 'adapter.h', 'adapter.cpp', 'bounded-tensor-validation.patch')} + for name in (CONSTRAINED_WRAPPERS if constrained else LEGACY_WRAPPERS)} require(value.get('wrapper_sources') == wrapper, 'native_wrapper_binding') builder.verify_loader_compilation(base / 'llama-build', base / 'llama-build/loader-overlay/llama-model-loader.cpp') require(type(value.get('dynamic_dependencies')) is list and value['dynamic_dependencies'] and len(set(value['dynamic_dependencies'])) == len(value['dynamic_dependencies']) and set(value['dynamic_dependencies']) <= builder.ALLOWED_NEEDED, 'native_dependency_binding') - return dict(kind=KIND, source_commit=SOURCE, source_tree=tree, build_manifest_sha256=report_hash, + result = dict(kind=KIND, source_commit=SOURCE, source_tree=tree, build_manifest_sha256=report_hash, library=library, wrapper_sources=wrapper, jobs=2, nice=19, cpu='avx2_fma_f16c', quantization=False, models_loaded=False) + if constrained: + result.update(build_version=3, output_constraints=[OUTPUT_CONSTRAINT]) + return result def validate_provision(core, base, report, build): @@ -176,20 +186,40 @@ def validate_provision(core, base, report, build): value, manifest_hash = document(root / 'backend.json', 16384) native = module(core / 'workers/volparossa-ml/llama_cpu.py', 'trial_native_contract') native.validate_manifest(value, require) + require(type(value.get('version')) is int and value['version'] in (1, 2), 'native_manifest_version') + constrained = value['version'] == 2 + require((value.get('output_constraints') == [OUTPUT_CONSTRAINT] + and build.get('build_version') == 3 and build.get('output_constraints') == [OUTPUT_CONSTRAINT]) + if constrained else ('output_constraints' not in value and 'output_constraints' not in build + and 'build_version' not in build), 'native_manifest_constraint') require(manifest_hash == backend['backend_sha256'] and value['model_profile'] == MODEL and value['build_manifest_sha256'] == build['build_manifest_sha256'] and value['library'] == {'path': 'libvolparossa_llama_cpu.so', **build['library']}, 'native_manifest_binding') copied_build, build_hash = document(root / 'build.json', 65536) require(build_hash == build['build_manifest_sha256'], 'native_copied_build_binding') native.validate_build_provenance(copied_build, require) + require(type(copied_build.get('version')) is int and copied_build['version'] == (3 if constrained else 2) + and (copied_build.get('output_constraints') == [OUTPUT_CONSTRAINT] if constrained + else 'output_constraints' not in copied_build), 'native_copied_build_constraint') for name, maximum in (('library', 128 * 1024 ** 2), ('gguf', 9 * 1024 ** 3)): require(value[name] == {'path': value[name]['path'], **file_identity(root / value[name]['path'], maximum)}, 'native_artifact_binding') # Closed hashes/sizes only: no host paths, prompts, source code or model text. - return dict(kind=KIND, source_commit=SOURCE, manifest_sha256=manifest_hash, + result = dict(kind=KIND, source_commit=SOURCE, manifest_sha256=manifest_hash, build_manifest_sha256=build_hash, library_sha256=value['library']['sha256'], gguf_sha256=value['gguf']['sha256'], gguf_bytes=value['gguf']['bytes'], source_weights_sha256=value['source_weights_sha256']) + if constrained: + result.update(backend_version=2, build_version=3, output_constraints=[OUTPUT_CONSTRAINT]) + return result + + +def require_output_constraint(build, backend): + """Current explicit trial cannot silently run against legacy native artifacts.""" + require(build.get('build_version') == 3 and backend.get('build_version') == 3 + and backend.get('backend_version') == 2 + and build.get('output_constraints') == backend.get('output_constraints') == [OUTPUT_CONSTRAINT], + 'native_output_constraint_required') def guest_build(deadline=None): diff --git a/scripts/smoke_opencode_inference.cjs b/scripts/smoke_opencode_inference.cjs index 61a0ec4..2b3c2bb 100644 --- a/scripts/smoke_opencode_inference.cjs +++ b/scripts/smoke_opencode_inference.cjs @@ -11,6 +11,7 @@ const {OpenCodeRuntime} = require('../src/opencode-runtime.cjs'); const {taskFailure} = require('../src/opencode-bridge.cjs'); const {createWorkspaceVerifier} = require('../src/workspace-verifier.cjs'); const {isCodingModel} = require('../src/opencode-config.cjs'); +const {OUTPUT_CONSTRAINT} = require('../src/private-conversation.cjs'); const ORIGINAL = 'def add(a, b):\n return a - b\n'; const TEST = 'import unittest\nfrom fixture import add\n\nclass AddTests(unittest.TestCase):\n' @@ -151,6 +152,28 @@ function guestGuard() { assert.equal(virt.status, 0); assert.equal(virt.stdout.trim(), 'kvm'); } +function trialArguments(args) { + assert.ok(args.length === 12 || args.length === 14); + assert.deepEqual(args.slice(0, 12).filter((_, index) => index < 2 || index % 2 === 0), + ['--execute', '--yes', '--node', '--build-report', '--socket', '--project-parent', '--output']); + if (args.length === 14) assert.deepEqual(args.slice(12), ['--output-constraint', OUTPUT_CONSTRAINT]); + const [, , , node, , buildReport, , socketPath, , parent, , output] = args; + return {node, buildReport, socketPath, parent, output, + ...(args.length === 14 ? {outputConstraint: OUTPUT_CONSTRAINT} : {})}; +} + +function selectionEvidence(outputConstraint) { + assert.ok(outputConstraint === undefined || outputConstraint === OUTPUT_CONSTRAINT); + return outputConstraint === undefined ? {} : {version: 2, output_constraint: OUTPUT_CONSTRAINT}; +} + +async function startTrialRuntime(config, project, outputConstraint) { + selectionEvidence(outputConstraint); // Same closed opt-in as the task receipt. + assert.equal(Object.hasOwn(config, 'outputConstraint'), false); + return OpenCodeRuntime.start({...config, + ...(outputConstraint !== undefined ? {outputConstraint} : {})}, {workspace: project}); +} + async function main(args = process.argv.slice(2)) { if (!args.length || args[0] === '--preview') { process.stdout.write(JSON.stringify({execute: false, actual_inference: false, @@ -158,10 +181,8 @@ async function main(args = process.argv.slice(2)) { usage: '--execute --yes --node ABS --build-report ABS --socket ABS --project-parent ABS --output NEW'}) + '\n'); return; } - assert.deepEqual(args.filter((_, index) => index < 2 || index % 2 === 0), - ['--execute', '--yes', '--node', '--build-report', '--socket', '--project-parent', '--output']); - assert.equal(args.length, 12); guestGuard(); - const [, , , node, , buildReport, , socketPath, , parent, , output] = args; + const {node, buildReport, socketPath, parent, output, outputConstraint} = trialArguments(args); + guestGuard(); await owned(parent, true); await owned(path.dirname(output), true); assert.ok(path.isAbsolute(output) && !path.basename(output).startsWith('.')); await fs.lstat(output).then(() => { throw Error('existing_output'); }, error => { if (error.code !== 'ENOENT') throw error; }); @@ -184,7 +205,8 @@ async function main(args = process.argv.slice(2)) { model_profile: null, approved_read: 0, approved_edit: 0, approved_test: 0, refused: 0, completed_commands: 0, failed_commands: 0, original_test_unchanged: false, fixture_changed: false, independent_test_passed: false, runtime_cleanup_confirmed: false, - project_removed: false, original_sha256: hash(ORIGINAL), resulting_sha256: null, elapsed_ms: 0}; + project_removed: false, original_sha256: hash(ORIGINAL), resulting_sha256: null, elapsed_ms: 0, + ...selectionEvidence(outputConstraint)}; const begin = Date.now(), controller = new AbortController(); const deadline = setTimeout(() => controller.abort(), 2400000); const interrupted = () => controller.abort(); @@ -199,7 +221,7 @@ async function main(args = process.argv.slice(2)) { assert.equal(await isolatedCheck(project, parent), false, 'fixture baseline must really fail'); const verify = createTrialVerifier(project); evidence.phase = 'runtime-start'; - runtime = await OpenCodeRuntime.start(config, {workspace: project}); + runtime = await startTrialRuntime(config, project, outputConstraint); bindRuntimeModel(evidence, runtime); evidence.phase = 'native-task'; await runNativeTrial(runtime, project, controller, evidence, verify); @@ -238,4 +260,5 @@ if (require.main === module) main().catch(() => { process.stderr.write('{"passed":false,"phase":"guard","failure":"guard_or_input_rejected"}\n'); process.exitCode = 1; }); module.exports = {main, commandKind, approvalKind, ORIGINAL, TEST, guestGuard, retainFailure, closeRuntime, - createTrialVerifier, runNativeTrial, bindRuntimeModel, isolatedCheck}; + createTrialVerifier, runNativeTrial, bindRuntimeModel, isolatedCheck, + trialArguments, selectionEvidence, startTrialRuntime}; diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index a1d21a2..b0e90e8 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -35,9 +35,10 @@ LARGE_CORE = '6a517b576baa17e7329661ee0476d1848081d114' MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') -# Native complete tensor validation is bounded and source-provenance checked. -NATIVE_CORE = '9e9086ee873f7162f498e8647e6f6f438273e6ce' +# Native complete tensor validation and opt-in output constraints are source-bound. +NATIVE_CORE = '9d044921f7e684737f9b10868429de42bc5416c9' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' +OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} IMAGE_NAME = 'debian-13-genericcloud-amd64-20260826-2582.qcow2' IMAGE_SHA512 = '184761b0dad0f9ace02f9298050ca96ce3caa39a461a47706d47ff9698b59933918b91b40177fbd4d392f6446af8b4d18ecb94caca988169b19641606bf34003' @@ -87,6 +88,23 @@ def validate_backend_fields(value, profile): == expected, 'input_backend') +def task_arguments(inference_backend='torch'): + require(inference_backend in INFERENCE_BACKENDS, 'unknown_inference_backend') + return [str(INPUT / 'runtime/node'), str(INPUT / 'code/scripts/smoke_opencode_inference.cjs'), + '--execute', '--yes', '--node', str(INPUT / 'runtime/node'), '--build-report', + str(INPUT / 'runtime/build-report.json'), '--socket', str(BASE / 'private.sock'), + '--project-parent', str(PROJECTS), '--output', str(BASE / 'task.json'), + *(['--output-constraint', OUTPUT_CONSTRAINT] if inference_backend != 'torch' else [])] + + +def validate_task_selection(task, profile, inference_backend='torch'): + require(inference_backend in INFERENCE_BACKENDS, 'unknown_inference_backend') + require(task['model_profile'] == profile['model_profile'], 'task_model_mismatch') + require(type(task.get('version')) is int and task['version'] == (1 if inference_backend == 'torch' else 2) + and (task.get('output_constraint') == OUTPUT_CONSTRAINT if inference_backend != 'torch' + else 'output_constraint' not in task), 'task_output_constraint_mismatch') + + def digest(path, algorithm='sha256'): with path.open('rb') as stream: return hashlib.file_digest(stream, algorithm).hexdigest() @@ -594,6 +612,7 @@ def guest(args): if backend != 'torch': native = module(INPUT / 'code/scripts/opencode_native_cpu.py', 'opencode_native_provision') report['native_backend'] = native.validate_provision(SOURCE, BASE, observed, report['native_build']) + native.require_output_constraint(report['native_build'], report['native_backend']) provision_stage = 'complete' report['phase'] = 'core-start' (BASE / 'work').mkdir(mode=0o700) @@ -612,10 +631,7 @@ def guest(args): require(int(state['MainPID']) > 0, 'core_process') report['phase'] = 'opencode-task' created.append(TASK_UNIT) - start(TASK_UNIT, [str(INPUT / 'runtime/node'), str(INPUT / 'code/scripts/smoke_opencode_inference.cjs'), - '--execute', '--yes', '--node', str(INPUT / 'runtime/node'), '--build-report', - str(INPUT / 'runtime/build-report.json'), '--socket', str(BASE / 'private.sock'), - '--project-parent', str(PROJECTS), '--output', str(BASE / 'task.json')], 768 * 1024**2, 2550) + start(TASK_UNIT, task_arguments(backend), 768 * 1024**2, 2550) deadline = time.monotonic() + 2565 while time.monotonic() < deadline: state = properties(TASK_UNIT) @@ -627,7 +643,7 @@ def guest(args): report['task_exit_status'] = int(state.get('ExecMainStatus', '-1')) if (BASE / 'task.json').exists(): report['task'] = load(BASE / 'task.json', 32768) - require(report['task']['model_profile'] == profile['model_profile'], 'task_model_mismatch') + validate_task_selection(report['task'], profile, backend) for name, field in ((CORE_UNIT, 'core_memory'), (TASK_UNIT, 'task_memory')): report[field] = memory(name) require(report[field]['swap'] == report[field]['oom_kill'] == 0, 'resource_violation') @@ -981,6 +997,8 @@ def interrupted(*_): validate_backend_fields(result, profile) require(result.get('core_revision') == profile['core_revision'] and result.get('model_profile') == profile['model_profile'], 'guest_source_profile') + if backend != 'torch' and result.get('passed') is True: + validate_task_selection(result.get('task', {}), profile, backend) receipt['actual_model_execution_proven'] = bool(result.get('passed') and result.get('actual_model_provisioned')) require(executed.returncode == 0 and result.get('passed') is True, 'guest_trial_failed') receipt['phase'], status = 'complete', 0 diff --git a/tests/smoke-opencode-inference.test.cjs b/tests/smoke-opencode-inference.test.cjs index d268409..8a4f9a9 100644 --- a/tests/smoke-opencode-inference.test.cjs +++ b/tests/smoke-opencode-inference.test.cjs @@ -7,8 +7,41 @@ const fs = require('node:fs/promises'); const path = require('node:path'); const os = require('node:os'); const {commandKind, approvalKind, ORIGINAL, TEST, retainFailure, closeRuntime, - createTrialVerifier, runNativeTrial, bindRuntimeModel} = require('../scripts/smoke_opencode_inference.cjs'); + createTrialVerifier, runNativeTrial, bindRuntimeModel, trialArguments, selectionEvidence, + startTrialRuntime} = require('../scripts/smoke_opencode_inference.cjs'); const {emptyTaskDiagnostic} = require('../src/opencode-bridge.cjs'); +const {OpenCodeRuntime} = require('../src/opencode-runtime.cjs'); +const PROFILE = 'qwen_tagged_tool_json_v1'; + +test('trial argument profile is exact, opt-in, and absent from original Torch receipt', () => { + const args = ['--execute', '--yes', '--node', '/inert/node', '--build-report', '/inert/build.json', + '--socket', '/inert/private.sock', '--project-parent', '/inert/projects', '--output', '/inert/task.json']; + const legacy = {node: '/inert/node', buildReport: '/inert/build.json', socketPath: '/inert/private.sock', + parent: '/inert/projects', output: '/inert/task.json'}; + assert.deepEqual(trialArguments(args), legacy); + assert.deepEqual(trialArguments([...args, '--output-constraint', PROFILE]), {...legacy, outputConstraint: PROFILE}); + assert.deepEqual(selectionEvidence(undefined), {}); + assert.deepEqual(selectionEvidence(PROFILE), {version: 2, output_constraint: PROFILE}); + for (const tail of [['--output-constraint'], ['--output-constraint', 'unknown'], ['--grammar', PROFILE], + ['--output-constraint', PROFILE, '--extra', 'value']]) assert.throws(() => trialArguments([...args, ...tail])); + for (const selection of [null, false, 1, '', 'greedy_v1', 'unknown']) assert.throws(() => selectionEvidence(selection)); +}); + +test('selected native trial reaches the real runtime entry with explicit config, without changing legacy config', async t => { + const observed = [], runtime = {synthetic: true}; + t.mock.method(OpenCodeRuntime, 'start', async (config, options) => { observed.push({config, options}); return runtime; }); + const config = {version: 1, opencode: '/inert/opencode', opencodeSha256: 'a'.repeat(64), + buildReport: '/inert/build.json', node: '/inert/node', nodeSha256: 'b'.repeat(64), socketPath: '/inert/private.sock'}; + const original = structuredClone(config); + assert.equal(await startTrialRuntime(config, '/inert/project'), runtime); + assert.equal(await startTrialRuntime(config, '/inert/project', PROFILE), runtime); + assert.deepEqual(observed, [{config: original, options: {workspace: '/inert/project'}}, + {config: {...original, outputConstraint: PROFILE}, options: {workspace: '/inert/project'}}]); + assert.deepEqual(config, original); + for (const value of [null, 'unknown']) await assert.rejects(startTrialRuntime(config, '/inert/project', value)); + await assert.rejects(startTrialRuntime({...config, outputConstraint: PROFILE}, '/inert/project')); + assert.equal(observed.length, 2); // Rejected selection cannot start any runtime. +}); async function trialProject(t) { const project = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-trial-wiring-')); diff --git a/tests/test_opencode_ci.py b/tests/test_opencode_ci.py index 17680ff..ad1b6d8 100644 --- a/tests/test_opencode_ci.py +++ b/tests/test_opencode_ci.py @@ -144,6 +144,13 @@ def test_default_guest_keeps_reviewed_task_and_acceptance_with_closed_diagnostic "trial_profile(getattr(args, 'model_profile', MODEL))"), ("staged_inputs(profile['model_profile'], backend)", "staged_inputs(profile['model_profile'])"), (" report.update(backend_fields(profile))\n", ''), + ("task_arguments(backend)", + "[str(INPUT / 'runtime/node'), str(INPUT / 'code/scripts/smoke_opencode_inference.cjs'),\n" + " '--execute', '--yes', '--node', str(INPUT / 'runtime/node'), '--build-report',\n" + " str(INPUT / 'runtime/build-report.json'), '--socket', str(BASE / 'private.sock'),\n" + " '--project-parent', str(PROJECTS), '--output', str(BASE / 'task.json')]"), + ("validate_task_selection(report['task'], profile, backend)", + "require(report['task']['model_profile'] == profile['model_profile'], 'task_model_mismatch')"), (" provision_pins = (model.load_pins(profile['model_profile'], native_cpu_converter=True)\n" " if backend != 'torch' else model.load_pins(profile['model_profile']))\n", " provision_pins = model.load_pins(profile['model_profile'])\n"), diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 4da81b9..417b5c4 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '9e9086ee873f7162f498e8647e6f6f438273e6ce') + self.assertEqual(TRIAL.NATIVE_CORE, '9d044921f7e684737f9b10868429de42bc5416c9') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): @@ -249,10 +249,11 @@ def test_guest_keeps_original_task_and_inference_budgets_and_joins_native_unit(s "if report['units_empty'] and report['provision_group_joined']:"): self.assertIn(text, source) task_driver = (ROOT / 'scripts/smoke_opencode_inference.cjs').read_bytes() - # Exact unchanged source at dd7a709, usable also in a shallow checkout; - # not regenerated tests, injected answers or a claim of model quality. + # Exact source with opt-in output selection, usable in a shallow checkout. + # Focused JS tests retain the original prompt, approvals and round bounds; + # no regenerated answers or claim of model quality. self.assertEqual(hashlib.sha256(task_driver).hexdigest(), - '702f0122475ca2e7da6b3f98b446071dcd3baf06af097168cffaa43631f1e484') + '92ca6da4a1cff07f07fd9e56afe28f853c2868f7d111c6cdc5350145bb54cba0') def test_native_memory_requires_actual_zero_swap_and_oom_not_just_limits(self): with tempfile.TemporaryDirectory() as temp, patch.object(TRIAL, 'cgroup', return_value=Path(temp)): @@ -690,7 +691,7 @@ def fail(native, listener, nonce, deadline, limit, observed): TRIAL.run_native_build(11 * TRIAL.GIB, {}) self.assertEqual(start.call_count, 1) - def bundle_fixture(self, directory): + def bundle_fixture(self, directory, constrained=False): base, core = Path(directory) / 'base', Path(directory) / 'core' build_root, backend = base / 'llama-build', base / 'ml/native-backend' wrappers = core / 'workers/volparossa-ml/native-cpu' @@ -702,18 +703,22 @@ def artifact(path, content): library = artifact(build_root / 'libvolparossa_llama_cpu.so', b'inert library bytes, never loaded') (backend / 'libvolparossa_llama_cpu.so').write_bytes((build_root / 'libvolparossa_llama_cpu.so').read_bytes()) wrapper = {name: artifact(wrappers / name, b'inert source ' + name.encode()) - for name in ('CMakeLists.txt', 'adapter.h', 'adapter.cpp', 'bounded-tensor-validation.patch')} - raw_build = dict(version=2, kind=KIND, source_commit=NATIVE.SOURCE, source_tree='b' * 40, + for name in (NATIVE.CONSTRAINED_WRAPPERS if constrained else NATIVE.LEGACY_WRAPPERS)} + raw_build = dict(version=3 if constrained else 2, kind=KIND, source_commit=NATIVE.SOURCE, source_tree='b' * 40, abi_version=1, jobs=2, cpu='avx2_fma_f16c', models_loaded=False, downloads=False, install=False, sanitizers=False, provisionable=True, quantization=False, library={'path': 'libvolparossa_llama_cpu.so', **library}, wrapper_sources=wrapper, dynamic_dependencies=['libc.so.6']) + if constrained: + raw_build['output_constraints'] = [NATIVE.OUTPUT_CONSTRAINT] for root in (build_root, backend): (root / 'build.json').write_text(json.dumps(raw_build)) build_hash = hashlib.sha256((build_root / 'build.json').read_bytes()).hexdigest() gguf = artifact(backend / 'model.gguf', b'inert gguf bytes, never loaded') - manifest = dict(model_profile=NATIVE.MODEL, build_manifest_sha256=build_hash, + manifest = dict(version=2 if constrained else 1, model_profile=NATIVE.MODEL, build_manifest_sha256=build_hash, library=raw_build['library'], gguf={'path': 'model.gguf', **gguf}, source_weights_sha256='c' * 64) + if constrained: + manifest['output_constraints'] = [NATIVE.OUTPUT_CONSTRAINT] (backend / 'backend.json').write_text(json.dumps(manifest)) report = dict(native_cpu_converter={'implementation': KIND, 'dependencies': {'sentencepiece': '0.2.1'}}, native_backend={'kind': KIND, 'root': str(backend), @@ -741,6 +746,8 @@ def test_exact_build_and_converted_manifest_bind_actual_inert_bytes(self): with patch.object(NATIVE, 'module', return_value=parser): result = NATIVE.validate_provision(core, base, report, build) self.assertEqual(result['manifest_sha256'], report['native_backend']['backend_sha256']) + self.assertEqual(set(result), {'kind', 'source_commit', 'manifest_sha256', 'build_manifest_sha256', + 'library_sha256', 'gguf_sha256', 'gguf_bytes', 'source_weights_sha256'}) self.assertNotIn(str(base), json.dumps(result)) for mutation in ({'native_cpu_converter': {}}, {'native_backend': dict(report['native_backend'], root='/untrusted')}, @@ -754,6 +761,73 @@ def test_exact_build_and_converted_manifest_bind_actual_inert_bytes(self): with patch.object(NATIVE, 'module', return_value=builder), self.assertRaisesRegex(ValueError, 'native_wrapper_binding'): NATIVE.validate_build(core, base) + def test_constrained_build3_backend2_preserve_closed_provenance_and_require_capability(self): + for selected in (False, True): + with self.subTest(selected=selected), tempfile.TemporaryDirectory() as temp: + base, core, report, builder, parser = self.bundle_fixture(temp, constrained=selected) + with patch.object(NATIVE, 'module', return_value=builder): + build = NATIVE.validate_build(core, base) + with patch.object(NATIVE, 'module', return_value=parser): + backend = NATIVE.validate_provision(core, base, report, build) + if selected: + NATIVE.require_output_constraint(build, backend) + self.assertEqual(build['build_version'], 3) + self.assertEqual(backend['build_version'], 3) + self.assertEqual(backend['backend_version'], 2) + self.assertEqual(build['output_constraints'], [NATIVE.OUTPUT_CONSTRAINT]) + self.assertEqual(backend['output_constraints'], [NATIVE.OUTPUT_CONSTRAINT]) + self.assertEqual(set(build['wrapper_sources']), set(NATIVE.CONSTRAINED_WRAPPERS)) + else: + with self.assertRaisesRegex(ValueError, 'native_output_constraint_required'): + NATIVE.require_output_constraint(build, backend) + self.assertNotIn('build_version', build) + self.assertNotIn('output_constraints', build) + self.assertNotIn('backend_version', backend) + self.assertEqual(set(build['wrapper_sources']), set(NATIVE.LEGACY_WRAPPERS)) + self.assertFalse(build['models_loaded']) + self.assertNotIn('passed', backend) + self.assertNotIn(str(base), json.dumps(backend)) + + def test_constrained_build_cannot_omit_substitute_or_downgrade_wrapper_capability(self): + changes = ({'version': True}, {'version': 4}, {'version': 2}, {'output_constraints': None}, + {'output_constraints': []}, {'output_constraints': ['unknown']}, + {'output_constraints': [NATIVE.OUTPUT_CONSTRAINT, NATIVE.OUTPUT_CONSTRAINT]}) + for changed in changes: + with self.subTest(changed=changed), tempfile.TemporaryDirectory() as temp: + base, core, _, builder, _ = self.bundle_fixture(temp, constrained=True) + file = base / 'llama-build/build.json' + value = json.loads(file.read_text()) | changed + file.write_text(json.dumps(value)) + with patch.object(NATIVE, 'module', return_value=builder), self.assertRaises(ValueError): + NATIVE.validate_build(core, base) + for name in ('output-constraint.h', 'output-constraint.cpp'): + with self.subTest(name=name), tempfile.TemporaryDirectory() as temp: + base, core, _, builder, _ = self.bundle_fixture(temp, constrained=True) + (core / 'workers/volparossa-ml/native-cpu' / name).write_bytes(b'changed source') + with patch.object(NATIVE, 'module', return_value=builder), \ + self.assertRaisesRegex(ValueError, 'native_wrapper_binding'): + NATIVE.validate_build(core, base) + + def test_constrained_manifest_and_current_trial_reject_missing_or_mixed_capability(self): + for changes in ({'version': True}, {'version': 1}, {'version': 3}, {'output_constraints': None}, + {'output_constraints': []}, {'output_constraints': ['unknown']}): + with self.subTest(changes=changes), tempfile.TemporaryDirectory() as temp: + base, core, report, builder, parser = self.bundle_fixture(temp, constrained=True) + with patch.object(NATIVE, 'module', return_value=builder): + build = NATIVE.validate_build(core, base) + file = base / 'ml/native-backend/backend.json' + file.write_text(json.dumps(json.loads(file.read_text()) | changes)) + report['native_backend']['backend_sha256'] = hashlib.sha256(file.read_bytes()).hexdigest() + with patch.object(NATIVE, 'module', return_value=parser), self.assertRaises(ValueError): + NATIVE.validate_provision(core, base, report, build) + build = {'build_version': 3, 'output_constraints': [NATIVE.OUTPUT_CONSTRAINT]} + backend = build | {'backend_version': 2} + for old_build, old_backend in (({}, backend), (build, {}), (build | {'output_constraints': []}, backend), + (build, backend | {'backend_version': 1}), (build, backend | {'output_constraints': None})): + with self.subTest(build=old_build, backend=old_backend), \ + self.assertRaisesRegex(ValueError, 'native_output_constraint_required'): + NATIVE.require_output_constraint(old_build, old_backend) + def test_core_provenance_and_loader_rejections_are_not_swallowed(self): with tempfile.TemporaryDirectory() as temp: base, core, _report, builder, parser = self.bundle_fixture(temp) diff --git a/tests/test_smoke_opencode_inference.py b/tests/test_smoke_opencode_inference.py index d8ef9f3..793858e 100644 --- a/tests/test_smoke_opencode_inference.py +++ b/tests/test_smoke_opencode_inference.py @@ -19,6 +19,46 @@ class Contracts(unittest.TestCase): + def test_task_arguments_preserve_torch_and_only_append_native_selection(self): + legacy = [str(TRIAL.INPUT / 'runtime/node'), str(TRIAL.INPUT / 'code/scripts/smoke_opencode_inference.cjs'), + '--execute', '--yes', '--node', str(TRIAL.INPUT / 'runtime/node'), '--build-report', + str(TRIAL.INPUT / 'runtime/build-report.json'), '--socket', str(TRIAL.BASE / 'private.sock'), + '--project-parent', str(TRIAL.PROJECTS), '--output', str(TRIAL.BASE / 'task.json')] + self.assertEqual(TRIAL.task_arguments(), legacy) + self.assertEqual(TRIAL.task_arguments('torch'), legacy) + self.assertEqual(TRIAL.task_arguments('llama_cpp_bf16_v1'), + legacy + ['--output-constraint', 'qwen_tagged_tool_json_v1']) + for backend in (None, 'unknown'): + with self.assertRaisesRegex(ValueError, 'unknown_inference_backend'): + TRIAL.task_arguments(backend) + guest = inspect.getsource(TRIAL.guest) + self.assertIn('start(TASK_UNIT, task_arguments(backend), 768 * 1024**2, 2550)', guest) + self.assertIn("native.require_output_constraint(report['native_build'], report['native_backend'])", guest) + host = inspect.getsource(TRIAL.execute) + self.assertIn("if backend != 'torch' and result.get('passed') is True:\n" + " validate_task_selection(result.get('task', {}), profile, backend)", host) + self.assertLess(host.index("validate_task_selection(result.get('task', {})"), + host.index("receipt['actual_model_execution_proven'] = bool")) + + def test_task_receipt_selection_binds_exact_version_without_relabeling_legacy(self): + legacy = {'version': 1, 'model_profile': TRIAL.LARGE_MODEL, 'passed': False} + selected = legacy | {'version': 2, 'output_constraint': TRIAL.OUTPUT_CONSTRAINT} + profile = TRIAL.trial_profile(TRIAL.LARGE_MODEL) + TRIAL.validate_task_selection(legacy, profile) + TRIAL.validate_task_selection(selected, profile, 'llama_cpp_bf16_v1') + self.assertEqual(legacy, {'version': 1, 'model_profile': TRIAL.LARGE_MODEL, 'passed': False}) + self.assertFalse(selected['passed']) + for task, backend in ((legacy, 'llama_cpp_bf16_v1'), (selected, 'torch'), + (selected | {'output_constraint': None}, 'llama_cpp_bf16_v1'), + (selected | {'output_constraint': 'other'}, 'llama_cpp_bf16_v1'), + (legacy | {'version': True}, 'torch'), + (legacy | {'output_constraint': None}, 'torch')): + with self.subTest(task=task, backend=backend), \ + self.assertRaisesRegex(ValueError, 'task_output_constraint_mismatch'): + TRIAL.validate_task_selection(task, profile, backend) + with self.assertRaisesRegex(ValueError, 'task_model_mismatch'): + TRIAL.validate_task_selection(selected | {'model_profile': TRIAL.MODEL}, profile, 'llama_cpp_bf16_v1') + def archive(self, root, name='code/example.cjs', *, payload=b'synthetic source\n', sha=None, link=False, model_profile=TRIAL.MODEL, core_revision=None): manifest = dict(core_revision=core_revision or TRIAL.trial_profile(model_profile)['core_revision'], model_profile=model_profile, From 6aa6291a1d7e591d1bfaff1b276ca2695a3c5d93 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:57:46 +0200 Subject: [PATCH 19/39] docs(code): distinguish latest native failure and selected-output checks --- README.md | 10 +++++----- docs/OPENCODE.md | 26 +++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9ed991e..6df5ecb 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,11 @@ adapter, editor and lifecycle behavior. The complete model-driven OpenCode read/edit/test loop, native editor UI operation, protected peer execution and a finished immune-policy path remain unproved. -**Latest real native CPU trial:** the [4B run](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37671030306) -failed at the execution deadline during post-generation verification. The worker -reported 44 generated tokens internally, but no verified native turn, tool call, -edit or test completed. This is a different failure from the preceding JSON -rejection; neither proves a working coding loop. See the +**Latest real native CPU trial:** the [4B run](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37683661957) +completed model preparation but returned an invalid model result. No usable turn, +tool action, edit or test completed. An explicit tool-output constraint is now +wired into the core and OpenCode; its source checks do not yet prove a successful +model-driven coding task. See the [native CPU evidence](docs/OPENCODE.md#explicit-native-cpu-candidate). **Real public peer proof:** a separate owner command successfully obtained a diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 03468e7..f3a7042 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -971,7 +971,7 @@ The original bytes are retained without decoding or newline normalization. This failed run does not replace or relabel any earlier result or relax model, task, integrity, resource or deadline checks. -The next source-only pin is core +The subsequent source correction used core `9e9086ee873f7162f498e8647e6f6f438273e6ce` (tree `ecda529c486e4f759f15d107b5e09a7710bcbfa6`). Native execution now retains the actual complete initial source-shard measurement rather than reading the unused @@ -984,8 +984,8 @@ model, task, resource, governor and deadline settings remain unchanged. The pin includes main's filter broker and four-validator TEST milestone; the merge preserves both independent third-party provenance sections. A new actual -trial requires fresh exact-source CI and preflight. No new model trial has run -on this source correction, and the original failures above remain unchanged. +trial requires fresh exact-source CI and preflight. Its actual follow-up is +recorded below; the original failures above remain unchanged. Select the native backend consistently for source selection, packing and execution, with `--model-profile qwen3-4b-instruct-2507-v1`. The manual workflow @@ -1016,3 +1016,23 @@ Neither that result nor the latest deadline during post-generation verification proves a usable native turn or successful coding loop. The core's retained full upstream sanitizer failure on a disallowed quantized path also remains explicit; separate BF16/F32 checks do not erase that failure. + +The follow-up [trial `37683661957`](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37683661957) +on Code `45510055` / core `9e9086ee` passes native build, conversion and +provisioning, then produces one cleanup-confirmed `invalid_output` result. +Its task stops after 475,006 ms with no usable native turn, tool action, approved +read/edit/test or completed command. The fixture is unchanged and independent +verification is not reached. Runtime, project and guest-unit cleanup complete. +This is not the earlier execution-deadline failure or a successful coding loop. +The original artifact ZIP SHA-256 is +`93edd2b2b92db78a807a1de11a431afb38ede10443b965c9c4fe523eac75573f`; +original job log SHA-256 is +`e0b7f8f2c8073ade7ee168c2ef9a4efddc2f5b710f9686c8621fbdf68eb2cd17`. + +The next selected-output trial is described under +[core-selected coding profiles](#core-selected-coding-profiles). Its Code +source-check run `38672991166` passes 296 Node tests (one skip), seven build +contracts, 92 Python OpenCode checks (two explicit retained-source skips) and +ten session-selection checks. The actual checkout `9f79e931` merges `689c7dd` +into `714904a3` and has the exact candidate tree. Original log ZIP SHA-256: +`8b59657a74190a61d8d0cf9177d96c0d488aec064e38faa54c30c5bd710ea978`. From 70c72fefd6d7f18868496c4ad97ba1091d5defc3 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:42:34 +0200 Subject: [PATCH 20/39] docs(code): record first real native read and remaining failures --- README.md | 11 ++++++----- docs/OPENCODE.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6df5ecb..b803ac1 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,12 @@ adapter, editor and lifecycle behavior. The complete model-driven OpenCode read/edit/test loop, native editor UI operation, protected peer execution and a finished immune-policy path remain unproved. -**Latest real native CPU trial:** the [4B run](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37683661957) -completed model preparation but returned an invalid model result. No usable turn, -tool action, edit or test completed. An explicit tool-output constraint is now -wired into the core and OpenCode; its source checks do not yet prove a successful -model-driven coding task. See the +**Latest real native CPU trial:** the [4B run](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38675278393) +completed model preparation, returned one model-generated function call and +completed one OpenCode `read` action. Six other requests failed; no edit, +test or complete coding task is proved. The selected tool-output constraint and +passing source checks do not turn this partial result into end-to-end success. +See the [native CPU evidence](docs/OPENCODE.md#explicit-native-cpu-candidate). **Real public peer proof:** a separate owner command successfully obtained a diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index f3a7042..d71f57a 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -1036,3 +1036,34 @@ contracts, 92 Python OpenCode checks (two explicit retained-source skips) and ten session-selection checks. The actual checkout `9f79e931` merges `689c7dd` into `714904a3` and has the exact candidate tree. Original log ZIP SHA-256: `8b59657a74190a61d8d0cf9177d96c0d488aec064e38faa54c30c5bd710ea978`. + +The selected-output [trial `38675278393`](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38675278393) +on Code `6aa6291a1d7e591d1bfaff1b276ca2695a3c5d93` / core +`9d044921f7e684737f9b10868429de42bc5416c9` remains **failed**, with a new +partial result: one model-generated function call reaches the actual OpenCode +runtime and its `read` tool completes. Native build, conversion and provisioning +pass with backend manifest v2/build v3 and `qwen_tagged_tool_json_v1`. + +Provider counters record seven submitted requests: one completed function call +and six `execution_failed` errors, with cleanup confirmed for all seven. +Invalid-output and execution-budget-error counters are zero. The retained core +diagnostic contains only `result_observed`; it does not identify why the six +requests failed. The read tool's pending/running/completed counters are observed +transitions of one call, not three different reads. + +The task stops after 588,147 ms with `opencode_task_incomplete`, no changed +fixture, edit, test or independent verification, and no completed native turn. +`approved_read` remains zero: a completed runtime read is not a separately +approved read event. Core cgroup peak is 7,632,764,928 bytes within 11 GiB, with +zero OOM kills and swap. No raw model output is exported. + +Reported runtime/project, provisioning-group, guest-unit/private-data, +QEMU/scratch and owned CI-host cleanup complete. Scoped normalized route/DNS +observations match; raw IPv6 route bytes differ. This is not byte-identical host +state, a complete firewall audit, general coding quality or protected peer +execution. The prior invalid-output and deadline failures remain unchanged. + +Original artifact `11946278747` ZIP SHA-256: +`487bee78bb4857c6a5b48df7d69df21ceeb608ded9d9264122668480f62afa74`; +original job-log ZIP SHA-256: +`5ff8a4055bf966f192f80b366cbeb8428d77793c6ee6525b3109eecf292af36e`. From 374f47251b19604bef51215719d5e7fc6f1cb664 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:52:39 +0200 Subject: [PATCH 21/39] test(code): retain closed core diagnostics without changing task authority --- tests/test_smoke_opencode_inference.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_smoke_opencode_inference.py b/tests/test_smoke_opencode_inference.py index 793858e..79de2fb 100644 --- a/tests/test_smoke_opencode_inference.py +++ b/tests/test_smoke_opencode_inference.py @@ -19,6 +19,17 @@ class Contracts(unittest.TestCase): + def test_core_diagnostics_use_pinned_closed_collector_without_changing_task_authority(self): + guest = inspect.getsource(TRIAL.guest) + self.assertIn("private = module(SOURCE / 'tests/integration/agent-private-conversation.py'", guest) + self.assertEqual(guest.count("private.service_diagnostic(BASE / (CORE_UNIT + '.log'))"), 2) + final = guest.split(' finally:', 1)[1] + self.assertIn("private.service_diagnostic(BASE / (CORE_UNIT + '.log'))", final) + self.assertIn("report['passed'] = report['phase'] == 'complete' and report['failure'] is None", final) + start = inspect.getsource(TRIAL.start) + self.assertIn('--setenv=RUST_LOG=off,volparossa::compute::private_diagnostic=debug', start) + self.assertNotIn('core_diagnostics', inspect.getsource(TRIAL.validate_task_selection)) + def test_task_arguments_preserve_torch_and_only_append_native_selection(self): legacy = [str(TRIAL.INPUT / 'runtime/node'), str(TRIAL.INPUT / 'code/scripts/smoke_opencode_inference.cjs'), '--execute', '--yes', '--node', str(TRIAL.INPUT / 'runtime/node'), '--build-report', From e39267343612ed8b594d15db3e161465c25b8c2f Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:23:59 +0200 Subject: [PATCH 22/39] test(code): pin closed task diagnostics for native OpenCode trial --- docs/OPENCODE.md | 22 +++++++++++++++++++--- scripts/smoke_opencode_inference.py | 4 ++-- tests/test_opencode_native_cpu.py | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index d71f57a..12b42e7 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -88,11 +88,27 @@ test still fails on its previously recorded Q1_0/Q8_0 alignment error. See The earlier invalid JSON trial is not considered fixed by these contract checks. The next native trial is pinned to core -`9d044921f7e684737f9b10868429de42bc5416c9`, with matching build v3/backend v2 +`5607f1177dc230a71414e7468c221b950f43bca6`, with matching build v3/backend v2 provenance and an explicit selector passed into the actual OpenCode runtime. The native guest and outer verifier require task receipt v2 with that selector; -legacy Torch arguments and task receipt v1 remain unchanged. Source checks still -gate the next real edit/test trial; no new model run has been dispatched. +legacy Torch arguments and task receipt v1 remain unchanged. + +The last completed trial on core `9d044921` reached one actual OpenCode read, +then stopped without an edit or test; its six execution failures lacked an +identified cause. The new pin retains closed failure codes at the private-task +boundary, including admission before worker startup and result validation after +execution. Only the existing explicitly enabled diagnostic channel observes +these codes. Raw errors, paths, prompts and model output remain excluded; +unknown errors stay unclassified. No model, resource limit, timeout, retry, +cleanup rule or task-acceptance condition changes. This prepares diagnosis of +the next trial, not a claim that the six failures are fixed. Source checks still +gate the next real edit/test trial. + +[Core Quality](https://github.com/VOLPAROSSA/volparossa/actions/runs/38678859785) +passes on that exact pin, including the three new task-diagnostic Rust tests +and all thirteen output-constraint Rust cases. The Code pin change passes 103 +targeted Python checks, with two retained-source checks explicitly skipped. +These results check source and diagnostic contracts, not actual model execution. - `qwen3-0.6b-v1`: the unchanged default; native template `qwen3-tools-nonthinking-v1`, model context 32,768 tokens. diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index b0e90e8..21aad89 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -35,8 +35,8 @@ LARGE_CORE = '6a517b576baa17e7329661ee0476d1848081d114' MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') -# Native complete tensor validation and opt-in output constraints are source-bound. -NATIVE_CORE = '9d044921f7e684737f9b10868429de42bc5416c9' +# Native validation, output constraints and closed task diagnostics are source-bound. +NATIVE_CORE = '5607f1177dc230a71414e7468c221b950f43bca6' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 417b5c4..ac70735 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '9d044921f7e684737f9b10868429de42bc5416c9') + self.assertEqual(TRIAL.NATIVE_CORE, '5607f1177dc230a71414e7468c221b950f43bca6') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 7f451e67629ff6db6e8348cc00587012240fea6f Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:48:07 +0200 Subject: [PATCH 23/39] test(opencode): pin bounded native cache-hint experiment --- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 21aad89..72ce04a 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = '5607f1177dc230a71414e7468c221b950f43bca6' +NATIVE_CORE = '3630ff7905f401f26245f2e6cf4f0f01d7950549' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index ac70735..4b8baab 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '5607f1177dc230a71414e7468c221b950f43bca6') + self.assertEqual(TRIAL.NATIVE_CORE, '3630ff7905f401f26245f2e6cf4f0f01d7950549') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 8b214da19ca61bfad631a5dcc09415bfeb7940c1 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:11:03 +0200 Subject: [PATCH 24/39] feat: retain bounded native memory diagnostics in Code trials --- scripts/opencode_ci.py | 4 + scripts/smoke_opencode_inference.py | 17 ++++- tests/test_opencode_ci.py | 102 +++++++++++++++++++++++++ tests/test_opencode_native_cpu.py | 2 +- tests/test_smoke_opencode_inference.py | 47 +++++++++++- 5 files changed, 164 insertions(+), 8 deletions(-) diff --git a/scripts/opencode_ci.py b/scripts/opencode_ci.py index 14d8a19..072c80d 100644 --- a/scripts/opencode_ci.py +++ b/scripts/opencode_ci.py @@ -237,6 +237,10 @@ def export(): and not info.st_mode & 0o077 and info.st_size <= 256 * 1024, 'closed_receipt_file') value = json.loads(source.read_bytes()) require(type(value) is dict, 'closed_receipt_object') + # Pretty-printing must not expand a bounded input beyond the same + # public-receipt ceiling. No raw logs or additional paths are added. + encoded = json.dumps(value, sort_keys=True, indent=2, allow_nan=False).encode() + b'\n' + require(len(encoded) <= 256 * 1024, 'closed_receipt_file') record(output / name, value) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 72ce04a..a156709 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = '3630ff7905f401f26245f2e6cf4f0f01d7950549' +NATIVE_CORE = '0091e75f49552ab5c5a0934cab6ae08463c45b17' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} @@ -128,6 +128,13 @@ def load(path, maximum=2 * 1024**2): return json.loads(path.read_bytes()) +def guest_receipt_limit(inference_backend='torch'): + require(inference_backend in INFERENCE_BACKENDS, 'unknown_inference_backend') + # The optional native diagnostic fits the existing public-export ceiling. + # This changes no task input, model, execution or resource allowance. + return 65536 if inference_backend == 'torch' else 256 * 1024 + + def closed_conversion(lines, log_state, process_status): """A diagnostic can report failure, never establish successful conversion.""" value = dict(state='absent', stage='unknown', failure='unknown', child_exit_status=None) @@ -647,7 +654,8 @@ def guest(args): for name, field in ((CORE_UNIT, 'core_memory'), (TASK_UNIT, 'task_memory')): report[field] = memory(name) require(report[field]['swap'] == report[field]['oom_kill'] == 0, 'resource_violation') - report['core_diagnostics'] = private.service_diagnostic(BASE / (CORE_UNIT + '.log')) + report['core_diagnostics'] = private.service_diagnostic( + BASE / (CORE_UNIT + '.log'), **({'memory_enabled': True} if backend != 'torch' else {})) require(report['task_exit_status'] == 0 and report.get('task', {}).get('passed') is True, 'task_failed') require(empty(TASK_UNIT) and not list((BASE / 'work').iterdir()), 'task_private_cleanup') require(staged_inputs(profile['model_profile'], backend) == manifest, 'staged_inputs_changed') @@ -685,7 +693,8 @@ def guest(args): unit(name, 'stop', check=False) if (BASE / (CORE_UNIT + '.log')).exists(): try: - report['core_diagnostics'] = private.service_diagnostic(BASE / (CORE_UNIT + '.log')) + report['core_diagnostics'] = private.service_diagnostic( + BASE / (CORE_UNIT + '.log'), **({'memory_enabled': True} if backend != 'torch' else {})) except (OSError, ValueError, KeyError): report['failure'] = 'closed_diagnostic_failed' report['units_empty'] = all(empty(name) for name in created) @@ -993,7 +1002,7 @@ def interrupted(*_): '--inference-backend', backend, timeout=6600, check=False) receipt['guest_exit_status'] = executed.returncode scp('vpci@127.0.0.1:/home/vpci/opencode-result.json', args.output / 'guest-result.json') - result = load(args.output / 'guest-result.json', 65536) + result = load(args.output / 'guest-result.json', guest_receipt_limit(backend)) validate_backend_fields(result, profile) require(result.get('core_revision') == profile['core_revision'] and result.get('model_profile') == profile['model_profile'], 'guest_source_profile') diff --git a/tests/test_opencode_ci.py b/tests/test_opencode_ci.py index ad1b6d8..b89e9ed 100644 --- a/tests/test_opencode_ci.py +++ b/tests/test_opencode_ci.py @@ -26,6 +26,51 @@ def load(name): TRIAL = load('smoke_opencode_inference') +def specialize_memory_diagnostics(source): + """Normalize only the two exact reviewed native opt-ins, never the golden.""" + keyword = ", **({'memory_enabled': True} if backend != 'torch' else {})" + expected = ast.parse("private.service_diagnostic(BASE / (CORE_UNIT + '.log')" + keyword + ')', + mode='eval').body + calls = [node for node in ast.walk(ast.parse(source)) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) and node.func.attr == 'service_diagnostic'] + if source.count(keyword) != 2 or len(calls) != 2 or any( + ast.dump(call) != ast.dump(expected) for call in calls): + raise ValueError('memory_diagnostic_specialization') + return source.replace(keyword, '') + + +def bounded_memory_receipt(): + """Maximum-count synthetic closed records, not observed model/runtime facts.""" + maximum = 2**53 - 1 + stat = dict(state='observed', **{key: maximum for key in ( + 'anon_bytes', 'file_bytes', 'shmem_bytes', 'inactive_file_bytes', 'active_file_bytes', 'kernel_bytes')}) + sample = dict(headroom_bytes=1, host_available_bytes=maximum, + self_headroom_bytes=1, ancestor_headroom_bytes=2, self_max_bytes=maximum, + self_current_bytes=maximum-1, ancestor_max_bytes=maximum, ancestor_current_bytes=maximum-2, + limiting_scope='self', missing_stage='none', io_kind='none', numeric_overflow=False, + self_stat=stat, ancestor_stat=stat) + memory = dict(version=1, phase='cache_hint', binding='bound', outcome='advised', + supervisor_ok=True, before=sample, after=sample, syscall_error=None) + state = dict(version=1, last_phase='checkpoint', + substage=dict(stage='verify_after', state='complete', elapsed_ms=599999), + capacity=dict(decision='cancel', constraint='quiet_hold', cpu_some_avg10=99.99999999999999, + io_some_avg10=99.99999999999999, memory_bytes=2**64-1), + controls=dict(issued=128, acknowledged=128, last_issued='resume', last_acknowledged='resume'), + peak_rss_bytes=10 * 1024**3, + generation=dict(prompt_tokens=12288, threads=2, interop_threads=2, + cpu=dict(isa='NO AVX', avx2=False, avx512_bf16=False, amx_bf16=False, + amx_tile=False, mkldnn_available=False, mkldnn_enabled=False), + first_forward_started_ms=599997, first_forward_completed_ms=599998, + first_token_ms=599999, generated_tokens=1024, complete=True, elapsed_ms=599999)) + event = dict(version=1, phase='task', detail=dict(code='compute_private_process_cleanup_deadline', + io_kind='permission_denied', exit_code=255, signal=64, stderr_class='user_namespace')) + return dict(version=1, kind='opencode-real-inference-guest', passed=False, + core_revision=TRIAL.NATIVE_CORE, model_profile=TRIAL.LARGE_MODEL, inference_backend='llama_cpp_bf16_v1', + native_source_commit=TRIAL.NATIVE_SOURCE, actual_model_provisioned=False, + core_diagnostics=dict(version=2, events=[event] * 16, states=[state] * 16, + memory=[memory] * 16, truncated=False, unrecognized_record=False)) + + class Contracts(unittest.TestCase): def test_host_profile_is_explicit_and_refused_on_local_host(self): with patch.dict(os.environ, {}, clear=True), patch.object(CI, 'run') as process: @@ -121,6 +166,48 @@ def test_fixed_receipt_export_does_not_publish_private_sentinels(self): self.assertEqual({p.name for p in result.iterdir()}, {'ci-source.json', 'vm-result.json'}) self.assertNotIn('sentinel', ''.join(p.read_text() for p in result.iterdir())) + def test_large_closed_memory_receipt_reaches_existing_export_without_widening_torch(self): + value = bounded_memory_receipt() + diagnostic = value['core_diagnostics'] + raw_log_bytes = sum(len(marker) + len(json.dumps(row, separators=(',', ':'))) + 1 + for marker, rows in (('private_memory_diagnostic ', diagnostic['memory']), + ('private_execution_state ', diagnostic['states']), + ('private_execution_diagnostic ', diagnostic['events'])) for row in rows) + self.assertLess(raw_log_bytes, 65536) + self.assertLess(len(json.dumps(diagnostic['memory'][0], separators=(',', ':'))) + + len('private_memory_diagnostic ') + 1, 4096) + with tempfile.TemporaryDirectory() as tmp, patch.object(CI, 'BUILD', Path(tmp)), patch.object(CI, 'guard'): + root = Path(tmp) + (root / 'ci-vm').mkdir() + source = root / 'ci-vm/guest-result.json' + TRIAL.record(source, value) + self.assertGreater(source.stat().st_size, 65536) + self.assertLessEqual(source.stat().st_size, 256 * 1024) + result = TRIAL.load(source, TRIAL.guest_receipt_limit('llama_cpp_bf16_v1')) + self.assertEqual(result, value) + self.assertFalse(result['passed']) + TRIAL.validate_backend_fields(result, TRIAL.trial_profile(TRIAL.LARGE_MODEL, 'llama_cpp_bf16_v1')) + with self.assertRaisesRegex(ValueError, 'report_bound'): + TRIAL.load(source, TRIAL.guest_receipt_limit('torch')) + CI.export() + exported = root / 'ci-public-receipts/guest-result.json' + self.assertEqual(json.loads(exported.read_bytes()), value) + self.assertLessEqual(exported.stat().st_size, 256 * 1024) + + def test_export_ceiling_also_bounds_pretty_print_expansion(self): + with tempfile.TemporaryDirectory() as tmp, patch.object(CI, 'BUILD', Path(tmp)), patch.object(CI, 'guard'): + root = Path(tmp) + (root / 'ci-vm').mkdir() + source = root / 'ci-vm/guest-result.json' + value = {'synthetic_counters': [0] * 60000} + source.write_text(json.dumps(value, separators=(',', ':'))) + source.chmod(0o600) + self.assertLess(source.stat().st_size, 256 * 1024) + self.assertGreater(len(json.dumps(value, sort_keys=True, indent=2).encode()) + 1, 256 * 1024) + with self.assertRaisesRegex(ValueError, 'closed_receipt_file'): + CI.export() + self.assertFalse((root / 'ci-public-receipts/guest-result.json').exists()) + def test_default_guest_keeps_reviewed_task_and_acceptance_with_closed_diagnostics(self): # Resolve only explicit profile substitutions and closed diagnostics. # Every other guest statement must still be @@ -138,6 +225,7 @@ def test_default_guest_keeps_reviewed_task_and_acceptance_with_closed_diagnostic lines = source.splitlines(keepends=True) del lines[child.lineno - node.lineno:child.end_lineno - node.lineno + 1] source = ''.join(lines) + source = specialize_memory_diagnostics(source) changes = ( (" backend = getattr(args, 'inference_backend', 'torch')\n", ''), ("trial_profile(getattr(args, 'model_profile', MODEL), backend)", @@ -211,6 +299,20 @@ def stable(value): self.assertEqual(hashlib.sha256(encoded).hexdigest(), '5321af83db1d961df90ecdbea73235da224117de85e32cd13143cb9436c6c637') + def test_torch_specialization_rejects_changed_memory_opt_in_expressions(self): + current = (ROOT / 'scripts/smoke_opencode_inference.py').read_text() + node = next(node for node in ast.parse(current).body + if isinstance(node, ast.FunctionDef) and node.name == 'guest') + source = ast.get_source_segment(current, node) + self.assertNotIn('memory_enabled', specialize_memory_diagnostics(source)) + for old, new in (("'memory_enabled': True", "'memory_enabled': False"), + ("'memory_enabled': True", "'memory_enabled': 1"), + ("if backend != 'torch' else {}", "if backend == 'torch' else {}"), + ("if backend != 'torch' else {}", "if backend != 'torch' else {'memory_enabled': False}"), + ("CORE_UNIT + '.log'", "TASK_UNIT + '.log'")): + with self.subTest(new=new), self.assertRaisesRegex(ValueError, 'memory_diagnostic_specialization'): + specialize_memory_diagnostics(source.replace(old, new, 1)) + def test_workflow_has_one_manual_trial_and_closed_export_only(self): source = (ROOT / '.github/workflows/opencode-inference.yml').read_text() for required in ('workflow_dispatch:', 'cancel-in-progress: false', 'runs-on: ubuntu-24.04', diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 4b8baab..f19902a 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '3630ff7905f401f26245f2e6cf4f0f01d7950549') + self.assertEqual(TRIAL.NATIVE_CORE, '0091e75f49552ab5c5a0934cab6ae08463c45b17') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): diff --git a/tests/test_smoke_opencode_inference.py b/tests/test_smoke_opencode_inference.py index 79de2fb..ca58489 100644 --- a/tests/test_smoke_opencode_inference.py +++ b/tests/test_smoke_opencode_inference.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-3.0-only """Small pure input/resource contracts, not a model, VM or runtime proof.""" +import ast import hashlib import importlib.util import io @@ -10,7 +11,7 @@ import tempfile from types import SimpleNamespace import unittest -from unittest.mock import patch +from unittest.mock import Mock, patch SCRIPT = Path(__file__).resolve().parents[1] / 'scripts/smoke_opencode_inference.py' SPEC = importlib.util.spec_from_file_location('opencode_inference_trial', SCRIPT) @@ -19,17 +20,57 @@ class Contracts(unittest.TestCase): + def test_native_guest_receipt_limit_is_bounded_without_widening_torch(self): + self.assertEqual(TRIAL.guest_receipt_limit(), 65536) + self.assertEqual(TRIAL.guest_receipt_limit('torch'), 65536) + self.assertEqual(TRIAL.guest_receipt_limit('llama_cpp_bf16_v1'), 256 * 1024) + for backend in (None, '', 'llama_cpp', 'other'): + with self.assertRaisesRegex(ValueError, 'unknown_inference_backend'): + TRIAL.guest_receipt_limit(backend) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / 'receipt.json' + # Padding is inert JSON whitespace, not a private payload or model. + path.write_bytes(b'{}' + b' ' * (256 * 1024 - 2)) + self.assertEqual(TRIAL.load(path, TRIAL.guest_receipt_limit('llama_cpp_bf16_v1')), {}) + with self.assertRaisesRegex(ValueError, 'report_bound'): + TRIAL.load(path, TRIAL.guest_receipt_limit('torch')) + path.write_bytes(path.read_bytes() + b' ') + with self.assertRaisesRegex(ValueError, 'report_bound'): + TRIAL.load(path, TRIAL.guest_receipt_limit('llama_cpp_bf16_v1')) + host = inspect.getsource(TRIAL.execute) + self.assertIn("load(args.output / 'guest-result.json', guest_receipt_limit(backend))", host) + self.assertIn('validate_backend_fields(result, profile)', host) + self.assertIn("require(executed.returncode == 0 and result.get('passed') is True, 'guest_trial_failed')", host) + def test_core_diagnostics_use_pinned_closed_collector_without_changing_task_authority(self): guest = inspect.getsource(TRIAL.guest) self.assertIn("private = module(SOURCE / 'tests/integration/agent-private-conversation.py'", guest) - self.assertEqual(guest.count("private.service_diagnostic(BASE / (CORE_UNIT + '.log'))"), 2) + self.assertEqual(guest.count('private.service_diagnostic('), 2) final = guest.split(' finally:', 1)[1] - self.assertIn("private.service_diagnostic(BASE / (CORE_UNIT + '.log'))", final) + self.assertIn('private.service_diagnostic(', final) self.assertIn("report['passed'] = report['phase'] == 'complete' and report['failure'] is None", final) start = inspect.getsource(TRIAL.start) self.assertIn('--setenv=RUST_LOG=off,volparossa::compute::private_diagnostic=debug', start) self.assertNotIn('core_diagnostics', inspect.getsource(TRIAL.validate_task_selection)) + def test_memory_collector_opt_in_is_native_only_at_both_existing_call_sites(self): + # Evaluate the two production call expressions without running a guest, + # loading a model or contacting any service. The second is in finally. + guest = ast.parse(inspect.getsource(TRIAL.guest)) + calls = [node for node in ast.walk(guest) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) and node.func.attr == 'service_diagnostic'] + self.assertEqual(len(calls), 2) + for call in calls: + for backend in ('torch', 'llama_cpp_bf16_v1'): + private = SimpleNamespace(service_diagnostic=Mock(return_value={'inert': True})) + result = eval(compile(ast.Expression(call), '', 'eval'), + {'private': private, 'backend': backend, + 'BASE': Path('/inert/private'), 'CORE_UNIT': 'inert-core'}) + self.assertEqual(result, {'inert': True}) + expected = {} if backend == 'torch' else {'memory_enabled': True} + private.service_diagnostic.assert_called_once_with( + Path('/inert/private/inert-core.log'), **expected) + def test_task_arguments_preserve_torch_and_only_append_native_selection(self): legacy = [str(TRIAL.INPUT / 'runtime/node'), str(TRIAL.INPUT / 'code/scripts/smoke_opencode_inference.cjs'), '--execute', '--yes', '--node', str(TRIAL.INPUT / 'runtime/node'), '--build-report', From 0c28e818c71c0b66b180e303d36439760e992596 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:26:31 +0200 Subject: [PATCH 25/39] chore: pin native memory diagnostics lint correction --- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index a156709..3caa4ec 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = '0091e75f49552ab5c5a0934cab6ae08463c45b17' +NATIVE_CORE = '60a7386bd5dedf614a08be497b41963f842c9512' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index f19902a..1356e72 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '0091e75f49552ab5c5a0934cab6ae08463c45b17') + self.assertEqual(TRIAL.NATIVE_CORE, '60a7386bd5dedf614a08be497b41963f842c9512') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 4fc36928bb12ae16e609e45d39e7a6d66b0b0272 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:49:06 +0200 Subject: [PATCH 26/39] test(code): pin native source cache release candidate --- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 3caa4ec..7a2ea98 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = '60a7386bd5dedf614a08be497b41963f842c9512' +NATIVE_CORE = '2dd38983a504d2b3a8ee0e5805e95a44234c621d' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 1356e72..f8ab178 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '60a7386bd5dedf614a08be497b41963f842c9512') + self.assertEqual(TRIAL.NATIVE_CORE, '2dd38983a504d2b3a8ee0e5805e95a44234c621d') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 3a5c1f59b282952e378e9525f2cf47da43ee19b2 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:13:23 +0200 Subject: [PATCH 27/39] Pin native source verification lifecycle candidate --- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 7a2ea98..45b273f 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = '2dd38983a504d2b3a8ee0e5805e95a44234c621d' +NATIVE_CORE = '3327115e07d2c4fa976b2d0c625729a68df0aaf6' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index f8ab178..cc7ed24 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '2dd38983a504d2b3a8ee0e5805e95a44234c621d') + self.assertEqual(TRIAL.NATIVE_CORE, '3327115e07d2c4fa976b2d0c625729a68df0aaf6') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 8b21775e072d3182b3766622fd387208992786a1 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:20:18 +0200 Subject: [PATCH 28/39] Observe bounded host QEMU memory without altering inference limits --- scripts/opencode_ci.py | 17 +- scripts/opencode_qemu_memory.py | 346 ++++++++++++++++++++ scripts/smoke_opencode_inference.py | 26 ++ tests/test_opencode_qemu_memory.py | 484 ++++++++++++++++++++++++++++ 4 files changed, 870 insertions(+), 3 deletions(-) create mode 100644 scripts/opencode_qemu_memory.py create mode 100644 tests/test_opencode_qemu_memory.py diff --git a/scripts/opencode_ci.py b/scripts/opencode_ci.py index 072c80d..c995def 100644 --- a/scripts/opencode_ci.py +++ b/scripts/opencode_ci.py @@ -32,7 +32,8 @@ 'vgabios-stdvga.bin': ('/usr/share/seabios/vgabios-stdvga.bin', 'seabios')} EXPORTS = ('ci-source.json', 'ci-host-tools.json', 'ci-preflight.json', 'ci-build.json', 'ci-inputs.json', 'ci-build-cleanup.json', 'ci-host-cleanup.json') -VM_EXPORTS = ('vm-result.json', 'guest-result.json', 'host-state-before.json', 'host-state-after.json') +VM_EXPORTS = ('vm-result.json', 'guest-result.json', 'host-state-before.json', 'host-state-after.json', + 'qemu-memory.json') def require(value, reason): @@ -235,12 +236,22 @@ def export(): info = source.lstat() require(stat.S_ISREG(info.st_mode) and info.st_uid == os.getuid() and not info.st_mode & 0o077 and info.st_size <= 256 * 1024, 'closed_receipt_file') - value = json.loads(source.read_bytes()) + if name == 'qemu-memory.json': + observer = module(ROOT / 'scripts/opencode_qemu_memory.py', 'ci_qemu_memory') + value = observer.load(source) + sources = json.loads((BUILD / 'ci-source.json').read_bytes()) + vm = json.loads((BUILD / 'ci-vm/vm-result.json').read_bytes()) + require(value['source'] == {key: sources[key] for key in value['source']} + and all(value['source'][key] == vm[key] for key in + ('core_revision', 'model_profile', 'inference_backend')), 'qemu_memory_source') + else: + value = json.loads(source.read_bytes()) require(type(value) is dict, 'closed_receipt_object') # Pretty-printing must not expand a bounded input beyond the same # public-receipt ceiling. No raw logs or additional paths are added. encoded = json.dumps(value, sort_keys=True, indent=2, allow_nan=False).encode() + b'\n' - require(len(encoded) <= 256 * 1024, 'closed_receipt_file') + require(len(encoded) <= (observer.MAX_JSON if name == 'qemu-memory.json' else 256 * 1024), + 'closed_receipt_file') record(output / name, value) diff --git a/scripts/opencode_qemu_memory.py b/scripts/opencode_qemu_memory.py new file mode 100644 index 0000000..1d69435 --- /dev/null +++ b/scripts/opencode_qemu_memory.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-3.0-only +"""Bounded, read-only host observations; never admission or OOM-cause proof. + +The four retained samples are sequential observations, not atomic snapshots or +the composition at the kernel's memory.peak. No guest data or cgroup names leave +this module. The sampler owns its descriptors until it actually exits. +""" +import json +import os +from pathlib import Path, PurePosixPath +import re +import stat +import threading +import time + +CGROUP_ROOT = Path('/sys/fs/cgroup') +MAX_FILE = 32768 +MAX_JSON = 16384 +MAX_NUMBER = 2**53 - 1 +INTERVAL = 1.0 +MAX_SECONDS = 7200 +MAX_SAMPLES = 7200 +JOIN_SECONDS = 2.0 +EVENT_KEYS = ('max', 'oom', 'oom_kill', 'oom_group_kill') +STAT_KEYS = ('anon', 'file', 'shmem', 'kernel', 'pagetables', 'sec_pagetables', + 'file_dirty', 'file_writeback', 'inactive_file') +SCALARS = {'current_bytes': 'memory.current', 'peak_bytes': 'memory.peak', + 'maximum_bytes': 'memory.max', 'swap_current_bytes': 'memory.swap.current'} +STATES = ('observed', 'partial', 'rejected', 'numeric_overflow', 'identity_changed') +STOPS = ('owner_stop', 'sample_limit', 'bind_rejected', 'source_unavailable', + 'identity_changed', 'observer_error', 'join_timeout') + + +def require(value): + if not value: + raise ValueError('qemu_memory_contract') + + +def identity(info): + # Directory link counts can change when unrelated sibling units are created. + return info.st_dev, info.st_ino, info.st_mode, info.st_uid + + +def number(raw): + require(re.fullmatch(rb'[0-9]{1,20}', raw) is not None) + value = int(raw) + if value > MAX_NUMBER: + raise OverflowError('qemu_memory_number') + return value + + +def empty_sample(elapsed_ms, state): + return dict(elapsed_ms=elapsed_ms, state=state, maximum_unlimited=False, + **{key: None for key in SCALARS}, + events={key: None for key in EVENT_KEYS}, + events_local={key: None for key in EVENT_KEYS}, + stat={key: None for key in STAT_KEYS}) + + +class Reader: + def __init__(self, group, unit): + self.fds, self.chain = [], [] + try: + require(type(group) is str and type(unit) is str + and re.fullmatch(r'volparossa-opencode-vm-[0-9a-f]{12}\.service', unit) + and group.startswith('/user.slice/') and group.endswith('/' + unit)) + path = PurePosixPath(group) + require(str(path) == group and '..' not in path.parts and len(path.parts) <= 17) + flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC + root = os.open(CGROUP_ROOT, flags) + self.fds.append(root) + self.root_identity = identity(os.fstat(root)) + parent = root + for part in path.parts[1:]: + fd = os.open(part, flags, dir_fd=parent) + self.fds.append(fd) + info = os.fstat(fd) + require(info.st_dev == self.root_identity[0] and info.st_uid in (0, os.geteuid()) + and not stat.S_IMODE(info.st_mode) & 0o022) + self.chain.append((parent, part, fd, identity(info))) + parent = fd + self.directory = parent + self.check() + except BaseException: + self.close() + raise + + def check(self): + require(identity(CGROUP_ROOT.lstat()) == self.root_identity + and identity(os.fstat(self.fds[0])) == self.root_identity) + for parent, name, fd, expected in self.chain: + require(identity(os.fstat(fd)) == expected + and identity(os.stat(name, dir_fd=parent, follow_symlinks=False)) == expected) + + def read(self, name): + self.check() + fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC, + dir_fd=self.directory) + try: + before = os.fstat(fd) + require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 + and before.st_uid in (0, os.geteuid()) + and not stat.S_IMODE(before.st_mode) & 0o022) + parts, size = [], 0 + while size <= MAX_FILE: + data = os.read(fd, MAX_FILE + 1 - size) + if not data: + break + parts.append(data) + size += len(data) + after = os.fstat(fd) + named = os.stat(name, dir_fd=self.directory, follow_symlinks=False) + require(size <= MAX_FILE and after.st_nlink == named.st_nlink == 1 + and identity(after) == identity(before) and identity(named) == identity(before)) + self.check() + return b''.join(parts).strip() + finally: + os.close(fd) + + def sample(self, elapsed_ms): + result = empty_sample(elapsed_ms, 'observed') + issues = set() + try: + self.check() + except (OSError, ValueError): + return empty_sample(elapsed_ms, 'identity_changed') + for key, filename in SCALARS.items(): + try: + raw = self.read(filename) + if key == 'maximum_bytes' and raw == b'max': + result['maximum_unlimited'] = True + else: + result[key] = number(raw) + except OSError: + issues.add('partial') + except ValueError: + issues.add('rejected') + except OverflowError: + issues.add('numeric_overflow') + for key, filename, keys in (('events', 'memory.events', EVENT_KEYS), + ('events_local', 'memory.events.local', EVENT_KEYS), + ('stat', 'memory.stat', STAT_KEYS)): + try: + rows = {} + for line in self.read(filename).splitlines(): + pair = line.split() + require(len(pair) == 2 and re.fullmatch(rb'[a-z_]+', pair[0]) + and pair[0] not in rows) + rows[pair[0]] = pair[1] + values = {name: number(rows[name.encode()]) if name.encode() in rows else None for name in keys} + if any(value is None for value in values.values()): + issues.add('partial') + result[key] = values + except OSError: + issues.add('partial') + except ValueError: + issues.add('rejected') + except OverflowError: + issues.add('numeric_overflow') + try: + self.check() + except (OSError, ValueError): + return empty_sample(elapsed_ms, 'identity_changed') + result['state'] = next((state for state in reversed(STATES) if state in issues), 'observed') + return result + + def close(self): + while self.fds: + fd = self.fds.pop() + try: + os.close(fd) + except OSError: + pass # Never retry close: that descriptor number may be reused. + + +class Observer: + def __init__(self, group, unit, source): + self.source = dict(source) + self.started = time.monotonic() + self.stop_event = threading.Event() + self.thread = None + self.reader = None + self.baseline = None + # One immutable publication keeps a timed-out join from exposing a + # partially updated result. Published sample dictionaries are never edited. + self.published = (0, None, None, None, 'source_unavailable') + try: + validate_source(self.source) + self.reader = Reader(group, unit) + self.baseline = self.reader.sample(self.elapsed()) + self.thread = threading.Thread(target=self.run, name='qemu-memory-observer', daemon=True) + self.thread.start() + except (OSError, ValueError): + self.published = (0, None, None, None, 'bind_rejected') + if self.reader is not None: + self.reader.close() + self.thread = None + except Exception: + self.published = (0, None, None, None, 'observer_error') + if self.reader is not None: + self.reader.close() + self.thread = None + + def elapsed(self): + return min(MAX_NUMBER, max(0, int((time.monotonic() - self.started) * 1000))) + + def run(self): + count, last, highest, terminal, reason = 0, None, None, None, 'owner_stop' + try: + for _ in range(MAX_SAMPLES): + if self.stop_event.wait(INTERVAL): + break + if time.monotonic() - self.started >= MAX_SECONDS: + reason = 'sample_limit' + break + last = self.reader.sample(self.elapsed()) + count += 1 + if last['current_bytes'] is not None and (highest is None + or last['current_bytes'] > highest['current_bytes']): + highest = last + self.published = (count, last, highest, None, reason) + if last['state'] == 'identity_changed': + reason = 'identity_changed' + break + else: + reason = 'sample_limit' + terminal = self.reader.sample(self.elapsed()) + except Exception: + reason = 'observer_error' + finally: + self.reader.close() + self.published = (count, last, highest, terminal, reason) + + def finish(self): + self.stop_event.set() + if self.thread is not None: + self.thread.join(JOIN_SECONDS) + joined = self.thread is None or not self.thread.is_alive() + count, last, highest, terminal, reason = self.published + observed = [sample for sample in (self.baseline, highest, terminal if joined else None) + if sample is not None and sample['current_bytes'] is not None] + highest = max(observed, key=lambda sample: sample['current_bytes']) if observed else None + return dict(version=1, kind='qemu-host-memory-observation', diagnostic_only=True, + cause_proven=False, exact_peak_composition=False, source=self.source, + observer_started=self.thread is not None, observer_joined=joined, + stop_reason=reason if joined else 'join_timeout', periodic_samples=count, + interval_ms=int(INTERVAL * 1000), maximum_periodic_samples=MAX_SAMPLES, + maximum_duration_seconds=MAX_SECONDS, baseline=self.baseline, + last_observed=last, highest_sampled_current=highest, + terminal=terminal if joined else None) + + +def validate_source(value): + require(type(value) is dict and set(value) == { + 'code_revision', 'core_revision', 'model_profile', 'inference_backend'}) + require(all(type(value[key]) is str and re.fullmatch('[0-9a-f]{40}', value[key]) + for key in ('code_revision', 'core_revision'))) + require(value['model_profile'] == 'qwen3-4b-instruct-2507-v1' + and value['inference_backend'] == 'llama_cpp_bf16_v1') + + +def validate(value): + require(type(value) is dict and set(value) == { + 'version', 'kind', 'diagnostic_only', 'cause_proven', 'exact_peak_composition', 'source', + 'observer_started', 'observer_joined', 'stop_reason', 'periodic_samples', 'interval_ms', + 'maximum_periodic_samples', 'maximum_duration_seconds', 'baseline', 'last_observed', + 'highest_sampled_current', 'terminal'}) + require(type(value['version']) is int and value['version'] == 1 + and value['kind'] == 'qemu-host-memory-observation' and value['diagnostic_only'] is True + and value['cause_proven'] is False and value['exact_peak_composition'] is False) + validate_source(value['source']) + require(type(value['observer_started']) is bool and type(value['observer_joined']) is bool + and value['stop_reason'] in STOPS and type(value['periodic_samples']) is int + and 0 <= value['periodic_samples'] <= MAX_SAMPLES) + for key, expected in (('interval_ms', 1000), ('maximum_periodic_samples', 7200), + ('maximum_duration_seconds', 7200)): + require(type(value[key]) is int and value[key] == expected) + for key in ('baseline', 'last_observed', 'highest_sampled_current', 'terminal'): + sample = value[key] + if sample is None: + continue + require(type(sample) is dict and set(sample) == set(empty_sample(0, 'observed'))) + require(sample['state'] in STATES and type(sample['maximum_unlimited']) is bool) + for field in ('elapsed_ms', *SCALARS): + number_value = sample[field] + require((field != 'elapsed_ms' and number_value is None) or + type(number_value) is int and 0 <= number_value <= MAX_NUMBER) + require(not sample['maximum_unlimited'] or sample['maximum_bytes'] is None) + for field, keys in (('events', EVENT_KEYS), ('events_local', EVENT_KEYS), ('stat', STAT_KEYS)): + require(type(sample[field]) is dict and set(sample[field]) == set(keys)) + require(all(item is None or type(item) is int and 0 <= item <= MAX_NUMBER + for item in sample[field].values())) + counters = [sample[key] for key in SCALARS if key != 'maximum_bytes'] + counters += [item for field in ('events', 'events_local', 'stat') for item in sample[field].values()] + if sample['state'] == 'observed': + require(all(item is not None for item in counters) + and (sample['maximum_bytes'] is not None or sample['maximum_unlimited'])) + if sample['state'] == 'identity_changed': + require(all(item is None for item in counters) and sample['maximum_bytes'] is None + and sample['maximum_unlimited'] is False) + if not value['observer_joined']: + require(value['observer_started'] and value['stop_reason'] == 'join_timeout' and value['terminal'] is None) + else: + require(value['stop_reason'] != 'join_timeout') + if not value['observer_started']: + require(value['observer_joined'] and value['periodic_samples'] == 0 + and all(value[key] is None for key in ('last_observed', 'terminal'))) + if value['periodic_samples'] == 0: + require(value['last_observed'] is None) + else: + require(value['observer_started'] and value['last_observed'] is not None) + if value['highest_sampled_current'] is not None: + high = value['highest_sampled_current']['current_bytes'] + require(high is not None and all(sample is None or sample['current_bytes'] is None + or high >= sample['current_bytes'] + for sample in (value['baseline'], value['last_observed'], value['terminal']))) + require(len(json.dumps(value, allow_nan=False).encode()) <= MAX_JSON) + return value + + +def load(path): + def object_pairs(pairs): + value = {} + for key, item in pairs: + require(key not in value) + value[key] = item + return value + + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC) + try: + before = os.fstat(fd) + require(stat.S_ISREG(before.st_mode) and before.st_uid == os.geteuid() + and stat.S_IMODE(before.st_mode) == 0o600 and before.st_nlink == 1 + and 0 < before.st_size <= MAX_JSON) + raw = os.read(fd, MAX_JSON + 1) + after, named = os.fstat(fd), path.lstat() + require(len(raw) == before.st_size and identity(after) == identity(named) == identity(before) + and after.st_nlink == named.st_nlink == 1 + and before.st_size == after.st_size == named.st_size + and before.st_mtime_ns == after.st_mtime_ns == named.st_mtime_ns + and before.st_ctime_ns == after.st_ctime_ns == named.st_ctime_ns) + return validate(json.loads(raw, object_pairs_hook=object_pairs, + parse_constant=lambda _: require(False))) + finally: + os.close(fd) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 45b273f..1745122 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -820,6 +820,28 @@ def qemu_command(tools, scratch, firmware=None, model_profile=MODEL, inference_b '-sandbox', 'on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny'] +def start_qemu_memory(group, name, manifest, backend): + if backend != 'llama_cpp_bf16_v1': + return None + try: + observer = module(ROOT / 'scripts/opencode_qemu_memory.py', 'opencode_qemu_memory') + source = dict(code_revision=manifest['code_base_revision'], core_revision=manifest['core_revision'], + model_profile=manifest['model_profile'], inference_backend=backend) + return observer, observer.Observer(group, name, source) + except Exception: + # Diagnostics never change admission, the primary result or cleanup. + return None + + +def finish_qemu_memory(observation, output): + if observation is not None: + try: + observer, owner = observation + record(output / 'qemu-memory.json', observer.validate(owner.finish())) + except Exception: + pass # Missing diagnostics are unknown, never successful execution. + + def validate_bundle(path, model_profile=MODEL, inference_backend='torch'): profile = trial_profile(model_profile, inference_backend) canonical(path) @@ -895,6 +917,7 @@ def execute(args): os.chmod(scratch, 0o700) name = 'volparossa-opencode-vm-' + uuid.uuid4().hex[:12] + '.service' launched, status = False, 1 + qemu_memory = None receipt = dict(version=1, kind='opencode-inference-vm', passed=False, phase='prepare', failure=None, core_revision=profile['core_revision'], model_profile=profile['model_profile'], bundle_sha256=digest(args.bundle), bundle_bytes=args.bundle.stat().st_size, @@ -977,6 +1000,7 @@ def interrupted(*_): receipt['qemu_swap_max'] = int((group_path / 'memory.swap.max').read_text()) require(receipt['qemu_memory_max'] == profile['qemu_memory_bytes'] and receipt['qemu_swap_max'] == 0, 'qemu_resource_limits') + qemu_memory = start_qemu_memory(group, name, manifest, backend) print(json.dumps({'phase': 'guest-boot', 'qemu_pid': receipt['qemu_pid'], 'unit': name}), flush=True) for _ in range(180): if ssh('true', timeout=10, check=False).returncode == 0: @@ -1017,6 +1041,8 @@ def interrupted(*_): finally: for sig, previous in old_signals.items(): signal.signal(sig, previous) + # Stop/join the read-only observer before poweroff, unit stop or scratch removal. + finish_qemu_memory(qemu_memory, args.output) if launched: try: with (scratch / 'qemu.stderr').open('rb') as stream: diff --git a/tests/test_opencode_qemu_memory.py b/tests/test_opencode_qemu_memory.py new file mode 100644 index 0000000..e3c024e --- /dev/null +++ b/tests/test_opencode_qemu_memory.py @@ -0,0 +1,484 @@ +# SPDX-License-Identifier: GPL-3.0-only +"""Inert real-file/descriptor/thread tests, never a cgroup or QEMU launch.""" +import copy +import importlib.util +import inspect +import json +import os +from pathlib import Path +import stat +import tempfile +import threading +import unittest +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[1] + + +def module(name): + spec = importlib.util.spec_from_file_location(name, ROOT / 'scripts' / (name + '.py')) + value = importlib.util.module_from_spec(spec) + spec.loader.exec_module(value) + return value + + +M = module('opencode_qemu_memory') +TRIAL = module('smoke_opencode_inference') +CI = module('opencode_ci') +UNIT = 'volparossa-opencode-vm-' + 'a' * 12 + '.service' +GROUP = '/user.slice/user-1000.slice/' + UNIT +SOURCE = dict(code_revision='a' * 40, core_revision=TRIAL.NATIVE_CORE, + model_profile=TRIAL.LARGE_MODEL, inference_backend='llama_cpp_bf16_v1') + + +class Fixture(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.group = self.root / GROUP[1:] + self.group.mkdir(parents=True, mode=0o700) + for directory in (self.root / 'user.slice', self.group.parent): + directory.chmod(0o700) + self.patch = patch.object(M, 'CGROUP_ROOT', self.root) + self.patch.start() + self.addCleanup(self.patch.stop) + self.files = {'memory.current': '100', 'memory.peak': '200', 'memory.max': str(13 * 1024**3), + 'memory.swap.current': '0', 'memory.events': 'max 3\noom 2\noom_kill 1\noom_group_kill 0\n', + 'memory.events.local': 'max 1\noom 0\noom_kill 1\noom_group_kill 0\n', + 'memory.stat': '\n'.join(f'{key} {index}' for index, key in enumerate(M.STAT_KEYS))} + for name, value in self.files.items(): + self.write(name, value) + + def write(self, name, value): + path = self.group / name + path.write_text(value) + path.chmod(0o600) + + def reader(self): + reader = M.Reader(GROUP, UNIT) + self.addCleanup(reader.close) + return reader + + def receipt(self): + observer = M.Observer(GROUP, UNIT, SOURCE) + result = observer.finish() + self.assertTrue(result['observer_joined']) + self.assertFalse(observer.reader.fds) + return M.validate(result) + + def test_real_readonly_descriptor_chain_and_exact_counters(self): + reader = self.reader() + self.assertTrue(all(not os.get_inheritable(fd) for fd in reader.fds)) + sample = reader.sample(23) + self.assertEqual(sample['state'], 'observed') + self.assertEqual(sample['current_bytes'], 100) + self.assertEqual(sample['peak_bytes'], 200) + self.assertEqual(sample['events']['oom'], 2) + self.assertEqual(sample['events_local']['oom'], 0) + self.assertEqual(set(sample['stat']), set(M.STAT_KEYS)) + self.assertNotIn(str(self.root), json.dumps(sample)) + self.assertEqual({name: (self.group / name).read_text() for name in self.files}, self.files) + + def test_missing_file_and_missing_counter_are_null_not_zero(self): + reader = self.reader() + (self.group / 'memory.current').unlink() + self.write('memory.events.local', 'max 0\noom 0\noom_kill 0\n') + sample = reader.sample(0) + self.assertEqual(sample['state'], 'partial') + self.assertIsNone(sample['current_bytes']) + self.assertIsNone(sample['events_local']['oom_group_kill']) + self.assertEqual(sample['events_local']['oom'], 0) + + def test_unlimited_maximum_is_distinct_from_missing(self): + reader = self.reader() + self.write('memory.max', 'max') + sample = reader.sample(0) + self.assertIsNone(sample['maximum_bytes']) + self.assertTrue(sample['maximum_unlimited']) + (self.group / 'memory.max').unlink() + sample = reader.sample(0) + self.assertIsNone(sample['maximum_bytes']) + self.assertFalse(sample['maximum_unlimited']) + + def test_malformed_duplicate_and_oversized_counters_never_export_raw(self): + reader = self.reader() + for raw in ('-1', 'True', '1.5', '/private/canary', '9' * (M.MAX_FILE + 1)): + with self.subTest(raw=raw[:20]): + self.write('memory.current', raw) + sample = reader.sample(0) + self.assertEqual(sample['state'], 'rejected') + self.assertIsNone(sample['current_bytes']) + self.assertNotIn('/private/canary', json.dumps(sample)) + self.write('memory.current', '100') + self.write('memory.events', 'oom 0\noom 1\n') + sample = reader.sample(0) + self.assertEqual(sample['state'], 'rejected') + self.assertTrue(all(value is None for value in sample['events'].values())) + + def test_counter_overflow_is_unknown_and_new_kernel_keys_are_not_exported(self): + reader = self.reader() + self.write('memory.peak', str(2**64 - 1)) + self.write('memory.stat', self.files['memory.stat'] + '\nfuture_counter 123\n') + sample = reader.sample(0) + self.assertEqual(sample['state'], 'numeric_overflow') + self.assertIsNone(sample['peak_bytes']) + self.assertNotIn('future_counter', sample['stat']) + + def test_symlink_fifo_hardlink_and_world_writable_files_are_denied(self): + reader = self.reader() + target = self.group / 'memory.current' + target.unlink() + outside = self.root / 'outside' + outside.write_text('314159') + outside.chmod(0o600) + for kind in ('symlink', 'fifo', 'hardlink', 'mode'): + with self.subTest(kind=kind): + if kind == 'symlink': + target.symlink_to(outside) + elif kind == 'fifo': + os.mkfifo(target, 0o600) + elif kind == 'hardlink': + os.link(outside, target) + else: + target.write_text('314159') + target.chmod(0o666) + self.assertIsNone(reader.sample(0)['current_bytes']) + target.unlink() + + def test_foreign_uid_counter_is_denied_without_chown(self): + reader = self.reader() + real = os.fstat + def foreign(fd): + info = real(fd) + if stat.S_ISREG(info.st_mode): + values = list(info) + values[4] = os.geteuid() + 123 + return os.stat_result(values) + return info + with patch.object(M.os, 'fstat', side_effect=foreign): + sample = reader.sample(0) + self.assertEqual(sample['state'], 'rejected') + self.assertIsNone(sample['current_bytes']) + + def test_directory_replacement_is_not_observed_as_original_group(self): + reader = self.reader() + self.group.rename(self.group.with_name(UNIT + '.old')) + self.group.mkdir(mode=0o700) + self.write('memory.current', '999') + sample = reader.sample(0) + self.assertEqual(sample['state'], 'identity_changed') + self.assertIsNone(sample['current_bytes']) + + def test_parent_replacement_and_symlink_binding_are_rejected(self): + reader = self.reader() + parent = self.group.parent + parent.rename(parent.with_name('old-parent')) + parent.symlink_to(parent.with_name('old-parent'), target_is_directory=True) + self.assertEqual(reader.sample(0)['state'], 'identity_changed') + with self.assertRaises(OSError): + M.Reader(GROUP, UNIT) + + def test_unrelated_sibling_directory_changes_do_not_invalidate_group(self): + reader = self.reader() + (self.group.parent / 'unrelated.service').mkdir() + self.assertEqual(reader.sample(0)['state'], 'observed') + + def test_counter_replaced_during_read_is_rejected(self): + reader = self.reader() + real = os.read + changed = False + def replaced(fd, length): + nonlocal changed + value = real(fd, length) + if not changed: + changed = True + target = self.group / 'memory.current' + target.rename(target.with_name('old-current')) + self.write('memory.current', '999') + return value + with patch.object(M.os, 'read', side_effect=replaced): + sample = reader.sample(0) + self.assertIsNone(sample['current_bytes']) + + def test_only_exact_private_owned_unit_and_canonical_group_bind(self): + for group, unit in (('/system.slice/' + UNIT, UNIT), + (GROUP.replace('/user-1000.slice/', '/./user-1000.slice/'), UNIT), + (GROUP.replace('/user-1000.slice/', '/../user-1000.slice/'), UNIT), + (GROUP, 'other.service'), (GROUP, UNIT + '/extra')): + with self.subTest(group=group, unit=unit), self.assertRaises(ValueError): + M.Reader(group, unit) + + def test_actual_thread_stops_joins_and_closes_before_return(self): + value = self.receipt() + self.assertTrue(value['observer_started']) + self.assertEqual(value['stop_reason'], 'owner_stop') + self.assertIsNotNone(value['baseline']) + self.assertIsNotNone(value['terminal']) + self.assertEqual(value['highest_sampled_current']['current_bytes'], 100) + self.assertFalse(value['cause_proven']) + self.assertFalse(value['exact_peak_composition']) + + def test_actual_thread_retains_one_periodic_real_file_sample_then_joins(self): + sampled, calls = threading.Event(), [] + original = M.Reader.sample + def observe(reader, elapsed): + value = original(reader, elapsed) + calls.append(value) + if len(calls) >= 2: + sampled.set() + return value + with patch.object(M.Reader, 'sample', observe): + observer = M.Observer(GROUP, UNIT, SOURCE) + try: + self.write('memory.current', '777') + self.assertTrue(sampled.wait(2.0)) + finally: + value = M.validate(observer.finish()) + self.assertGreaterEqual(value['periodic_samples'], 1) + self.assertEqual(value['last_observed']['current_bytes'], 777) + self.assertEqual(value['highest_sampled_current']['current_bytes'], 777) + self.assertTrue(value['observer_joined']) + self.assertFalse(observer.reader.fds) + + def scripted(self, numbers): + observer = object.__new__(M.Observer) + observer.source, observer.started = dict(SOURCE), M.time.monotonic() + observer.reader = self.reader() + observer.baseline = observer.reader.sample(0) + observer.thread = None + observer.published = (0, None, None, None, 'source_unavailable') + sequence = iter(numbers) + def wait(_): + value = next(sequence, None) + if value is None: + return True + self.write('memory.current', str(value)) + self.write('memory.stat', self.files['memory.stat'].replace('file 1', f'file {value // 2}')) + return False + observer.stop_event = Mock(wait=wait) + return observer + + def test_last_and_highest_are_separate_sampled_compositions_not_kernel_peak(self): + observer = self.scripted([120, 180, 90]) + observer.run() + count, last, high, terminal, reason = observer.published + self.assertEqual((count, last['current_bytes'], high['current_bytes']), (3, 90, 180)) + self.assertEqual(high['stat']['file'], 90) + self.assertEqual(high['peak_bytes'], 200) + self.assertEqual(terminal['current_bytes'], 90) + self.assertEqual(reason, 'owner_stop') + self.assertFalse(observer.reader.fds) + + def test_sample_count_and_duration_are_independently_bounded(self): + observer = self.scripted([1, 2, 3]) + with patch.object(M, 'MAX_SAMPLES', 2): + observer.run() + self.assertEqual(observer.published[0], 2) + self.assertEqual(observer.published[-1], 'sample_limit') + observer = self.scripted([1, 2, 3]) + observer.started -= M.MAX_SECONDS + observer.run() + self.assertEqual(observer.published[0], 0) + self.assertEqual(observer.published[-1], 'sample_limit') + + def test_sampling_exception_closes_fds_and_keeps_finite_failure(self): + observer = self.scripted([1]) + with patch.object(observer.reader, 'sample', side_effect=RuntimeError('/private/canary')): + observer.run() + self.assertEqual(observer.published[-1], 'observer_error') + self.assertFalse(observer.reader.fds) + self.assertNotIn('/private/canary', repr(observer.published)) + + def test_binding_failure_and_thread_start_failure_are_closed_unknowns(self): + observer = M.Observer('/untrusted/private/path', UNIT, SOURCE) + value = M.validate(observer.finish()) + self.assertFalse(value['observer_started']) + self.assertIsNone(value['baseline']) + self.assertEqual(value['stop_reason'], 'bind_rejected') + self.assertNotIn('/untrusted/private/path', json.dumps(value)) + with patch.object(M.threading.Thread, 'start', side_effect=RuntimeError('/private/canary')): + observer = M.Observer(GROUP, UNIT, SOURCE) + value = M.validate(observer.finish()) + self.assertFalse(value['observer_started']) + self.assertEqual(value['stop_reason'], 'observer_error') + self.assertFalse(observer.reader.fds) + + def test_group_disappearance_stops_sampling_without_rebinding(self): + observer = self.scripted([1, 2, 3]) + real_wait = observer.stop_event.wait + def moved(interval): + result = real_wait(interval) + self.group.rename(self.group.with_name(UNIT + '.gone')) + return result + observer.stop_event.wait = moved + observer.run() + self.assertEqual(observer.published[0], 1) + self.assertEqual(observer.published[-1], 'identity_changed') + self.assertIsNone(observer.published[1]['current_bytes']) + self.assertFalse(observer.reader.fds) + + def test_join_timeout_does_not_close_live_readers_or_claim_terminal_sample(self): + observer = self.scripted([]) + observer.thread = Mock(is_alive=lambda: True) + value = observer.finish() + observer.thread.join.assert_called_once_with(2.0) + self.assertFalse(value['observer_joined']) + self.assertEqual(value['stop_reason'], 'join_timeout') + self.assertIsNone(value['terminal']) + self.assertTrue(observer.reader.fds) + M.validate(value) + + def test_close_error_never_retries_fd_or_masks_other_closes(self): + reader = self.reader() + fds = list(reader.fds) + real, calls = os.close, [] + def close(fd): + calls.append(fd) + real(fd) + if len(calls) == 1: + raise OSError('/private/canary') + with patch.object(M.os, 'close', side_effect=close): + reader.close() + self.assertEqual(calls, list(reversed(fds))) + self.assertFalse(reader.fds) + + def test_validator_rejects_unknown_fields_types_cause_claims_and_raw_strings(self): + value = self.receipt() + for mutation in ( + lambda v: v.update(version=True), lambda v: v.update(cause_proven=True), + lambda v: v.update(exact_peak_composition=True), lambda v: v.update(path='/private/canary'), + lambda v: v.update(stop_reason='/private/canary'), lambda v: v.update(periodic_samples=True), + lambda v: v['baseline'].update(current_bytes=True), + lambda v: v['baseline'].update(current_bytes=2**64), + lambda v: v['baseline'].update(state='observed', current_bytes=None), + lambda v: v['baseline'].update(state='identity_changed'), + lambda v: v['baseline']['stat'].update(secret='/private/canary'), + lambda v: v['source'].update(code_revision='unknown')): + changed = copy.deepcopy(value) + mutation(changed) + with self.assertRaises(ValueError): + M.validate(changed) + + def test_maximum_closed_receipt_stays_within_small_independent_export_cap(self): + value = self.receipt() + sample = M.empty_sample(M.MAX_NUMBER, 'observed') + for key in M.SCALARS: + sample[key] = M.MAX_NUMBER + for key in ('events', 'events_local', 'stat'): + sample[key] = dict.fromkeys(sample[key], M.MAX_NUMBER) + for key in ('baseline', 'last_observed', 'highest_sampled_current', 'terminal'): + value[key] = copy.deepcopy(sample) + value['periodic_samples'] = M.MAX_SAMPLES + M.validate(value) + self.assertLessEqual(len((json.dumps(value, indent=2) + '\n').encode()), M.MAX_JSON) + + def test_real_receipt_read_rejects_duplicate_nonfinite_links_and_modes(self): + value = self.receipt() + target = self.root / 'qemu-memory.json' + target.write_text(json.dumps(value)) + target.chmod(0o600) + self.assertEqual(M.load(target), value) + for raw in ('{"version":1,"version":1}', '{"version":NaN}'): + target.write_text(raw) + with self.assertRaises(ValueError): + M.load(target) + target.write_text(json.dumps(value)) + target.chmod(0o644) + with self.assertRaises(ValueError): + M.load(target) + target.chmod(0o600) + os.link(target, self.root / 'hardlink') + with self.assertRaises(ValueError): + M.load(target) + + def test_host_wrapper_only_native_no_effect_on_primary_results_or_cleanup(self): + with patch.object(TRIAL, 'module') as load: + self.assertIsNone(TRIAL.start_qemu_memory(GROUP, UNIT, {}, 'torch')) + load.assert_not_called() + with patch.object(TRIAL, 'module', side_effect=RuntimeError('/private/canary')): + self.assertIsNone(TRIAL.start_qemu_memory(GROUP, UNIT, {}, 'llama_cpp_bf16_v1')) + for failure in (OSError('/private/canary'), RuntimeError('/private/canary')): + owner = Mock(finish=Mock(side_effect=failure)) + TRIAL.finish_qemu_memory((M, owner), self.root) + self.assertFalse((self.root / 'qemu-memory.json').exists()) + source = inspect.getsource(TRIAL.execute) + self.assertLess(source.index('qemu_resource_limits'), source.index('start_qemu_memory(')) + final = source.split(' finally:', 1)[1] + self.assertLess(final.index('finish_qemu_memory('), final.index("ssh('sudo', '-n', 'systemctl', 'poweroff'")) + self.assertLess(final.index('finish_qemu_memory('), final.index('shutil.rmtree(scratch)')) + self.assertNotIn('qemu_memory', inspect.getsource(TRIAL.guest)) + self.assertNotIn('cache=', inspect.getsource(TRIAL.qemu_command)) + + def test_new_closed_export_binds_sources_and_preserves_legacy_receipts(self): + value = self.receipt() + build = self.root / 'build' + vm = build / 'ci-vm' + vm.mkdir(parents=True) + source = dict(SOURCE, version=1, unrelated_historical_field=True) + legacy = dict(version=1, core_revision=SOURCE['core_revision'], model_profile=SOURCE['model_profile'], + inference_backend=SOURCE['inference_backend'], passed=False, failure='stage_failed') + for path, data in ((build / 'ci-source.json', source), (vm / 'vm-result.json', legacy), + (vm / 'qemu-memory.json', value)): + path.write_text(json.dumps(data)) + path.chmod(0o600) + (vm / 'private-model.log').write_text('/private/canary') + with patch.object(CI, 'BUILD', build), patch.object(CI, 'guard'): + CI.export() + exported = build / 'ci-public-receipts' + self.assertEqual(json.loads((exported / 'vm-result.json').read_bytes()), legacy) + self.assertEqual(M.load(exported / 'qemu-memory.json'), value) + self.assertEqual(set(p.name for p in exported.iterdir()), {'ci-source.json', 'vm-result.json', 'qemu-memory.json'}) + self.assertNotIn('/private/canary', ''.join(p.read_text() for p in exported.iterdir())) + + def test_export_rejects_wrong_source_and_does_not_relabel_old_receipts(self): + value = self.receipt() + for include, change in ((False, False), (True, True)): + with self.subTest(include=include, change=change), tempfile.TemporaryDirectory() as directory: + build = Path(directory) + vm = build / 'ci-vm' + vm.mkdir() + legacy = dict(version=1, core_revision=SOURCE['core_revision'], model_profile=SOURCE['model_profile'], + inference_backend=SOURCE['inference_backend'], passed=False) + for path, data in ((build / 'ci-source.json', dict(SOURCE, code_revision='b' * 40)), + (vm / 'vm-result.json', legacy)): + path.write_text(json.dumps(data)) + path.chmod(0o600) + if include: + target = vm / 'qemu-memory.json' + target.write_text(json.dumps(value)) + target.chmod(0o600) + with patch.object(CI, 'BUILD', build), patch.object(CI, 'guard'): + if change: + with self.assertRaisesRegex(ValueError, 'qemu_memory_source'): + CI.export() + else: + CI.export() + self.assertEqual(json.loads((build / 'ci-public-receipts/vm-result.json').read_bytes()), legacy) + self.assertFalse((build / 'ci-public-receipts/qemu-memory.json').exists()) + + def test_read_flags_no_retry_and_producer_to_closed_export_pipeline(self): + real, opened = os.open, [] + def opened_readonly(path, flags, *args, **kwargs): + opened.append((path, flags)) + self.assertEqual(flags & os.O_ACCMODE, os.O_RDONLY) + self.assertTrue(flags & os.O_NOFOLLOW) + self.assertTrue(flags & os.O_CLOEXEC) + if path in self.files: + self.assertTrue(flags & os.O_NONBLOCK) + return real(path, flags, *args, **kwargs) + manifest = dict(SOURCE, code_base_revision=SOURCE['code_revision']) + with patch.object(M.os, 'open', side_effect=opened_readonly), patch.object(TRIAL, 'module', return_value=M): + observation = TRIAL.start_qemu_memory(GROUP, UNIT, manifest, 'llama_cpp_bf16_v1') + TRIAL.finish_qemu_memory(observation, self.root) + value = M.load(self.root / 'qemu-memory.json') + self.assertEqual(value['source'], SOURCE) + self.assertTrue(value['observer_joined']) + self.assertTrue(opened) + # Missing/failed diagnostic output cannot become a new task failure. + with patch.object(TRIAL, 'record', side_effect=OSError('/private/canary')): + TRIAL.finish_qemu_memory(observation, self.root) + + +if __name__ == '__main__': + unittest.main() From 97dfa4adb910970128b6f47f2f65c28f6201abca Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:18:13 +0200 Subject: [PATCH 29/39] feat: keep native model sessions within one owner coding task --- README.md | 12 +- docs/OPENCODE.md | 34 +++- scripts/opencode_native_cpu.py | 48 ++++-- scripts/opencode_session.cjs | 16 +- scripts/opencode_session.py | 2 +- scripts/pack_opencode_cooperation.py | 2 +- src/chat-completions-provider.cjs | 92 ++++++++-- src/private-conversation.cjs | 29 +++- src/private-native-session.cjs | 159 +++++++++++++++++ tests/opencode-session.test.cjs | 5 +- tests/private-native-session.test.cjs | 217 ++++++++++++++++++++++++ tests/test_opencode_native_cpu.py | 148 +++++++++++++++- tests/test_pack_opencode_cooperation.py | 10 +- 13 files changed, 712 insertions(+), 62 deletions(-) create mode 100644 src/private-native-session.cjs create mode 100644 tests/private-native-session.test.cjs diff --git a/README.md b/README.md index b803ac1..76859df 100644 --- a/README.md +++ b/README.md @@ -74,12 +74,12 @@ adapter, editor and lifecycle behavior. The complete model-driven OpenCode read/edit/test loop, native editor UI operation, protected peer execution and a finished immune-policy path remain unproved. -**Latest real native CPU trial:** the [4B run](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38675278393) -completed model preparation, returned one model-generated function call and -completed one OpenCode `read` action. Six other requests failed; no edit, -test or complete coding task is proved. The selected tool-output constraint and -passing source checks do not turn this partial result into end-to-end success. -See the +**Latest real native CPU trial:** the [4B run](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38742699743) +completed two OpenCode `read` actions, then reached the third model request's +compute deadline. The project remained unchanged; no edit or test passed. +The next candidate reuses the model and matching prompt prefixes within one +owner task, with the ordinary toolset and bounded resources. Its transport tests +do not yet prove faster inference or a complete coding task. See the [native CPU evidence](docs/OPENCODE.md#explicit-native-cpu-candidate). **Real public peer proof:** a separate owner command successfully obtained a diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 12b42e7..aaeaecf 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -60,6 +60,31 @@ owner-selected check after each completed coding turn. Each invocation requires separate approval; an actual failed check can continue the same session within the original task budget. See [editor setup and check limitations](OWNER_VERIFICATION.md#use-from-the-editor). +### Connection-bound native execution candidate + +The native CPU path now requests a connection-bound session from the core. +One OpenCode task can reuse the resident model and exact already-processed +prompt prefixes across its ordinary tool requests. The core owns the session; +there is no reusable session token or cache shared across projects or owners. +This requires the explicit `native_session_version: 1` capability and matching +build v4/backend v3 provenance. Older cores fail closed on this path; the legacy +non-native conversation interface remains unchanged. + +Each result confirms removal of that request's files, not termination of the +resident worker. The task waits for a separate, correlated session-close receipt +before reporting final worker/private-state cleanup. A disconnect or cancellation +acknowledgement is not that proof. An admitted failed request is never replayed; +a verified idle closure permits only a new request on a new, empty session. + +Limits remain 600 seconds per request, at most 128 requests, 300 idle seconds and +2,400 seconds for the owner task. The native worker's cumulative CPU ceiling +covers that task lifetime; each request retains its own CPU allowance and the +existing pressure and memory checks. Tool descriptions, context limits, model, +workspace permissions and independent task checks are not reduced for the trial. +Synthetic Unix/HTTP tests verify the new correlation and cleanup contracts; +actual model reuse, acceleration and successful editing remain to be demonstrated. +This private-local execution slice does not complete protected peer execution. + ### Core-selected coding profiles An experimental owner-selected `outputConstraint: "qwen_tagged_tool_json_v1"` @@ -162,8 +187,11 @@ alignment does not itself prove that the model can complete a coding task. Supported message/tool history and call identities are preserved. Unsupported inputs fail rather than being silently shortened or stripped. The provider waits -for core cleanup before returning SDK-compatible SSE or JSON; this is not -token-by-token model streaming. Exhaustion remains `length`, not successful `stop`. +for the core's applicable cleanup receipt before returning SDK-compatible SSE or +JSON: request-file cleanup for a resident native session, complete execution +cleanup for the legacy one-shot path. Final native worker cleanup is checked at +task close. This is not token-by-token model streaming. Exhaustion remains +`length`, not successful `stop`. Cleanup-confirmed invalid/truncated output or an unmet tool choice returns a terminal HTTP 422, so the pinned SDK and OpenCode do not blindly regenerate that unusable turn. The negotiated `execution_budget_exceeded` response also maps to @@ -496,7 +524,7 @@ python3 -B scripts/pack_opencode_cooperation.py --execute \ --output /absolute/code-worktree/build/opencode-cooperative-inputs-01 ``` -The manifest binds all 25 source/runtime/license files by hash, size and mode. +The current manifest binds all 26 source/runtime/license files by hash, size and mode. The packer reads source blobs at the selected commit, not uncommitted changes, and verifies the existing source-build record and pinned Node distribution. It does not download or launch anything. Pass this new directory and the reported diff --git a/scripts/opencode_native_cpu.py b/scripts/opencode_native_cpu.py index 0347696..cd782d6 100644 --- a/scripts/opencode_native_cpu.py +++ b/scripts/opencode_native_cpu.py @@ -24,8 +24,10 @@ ORIGIN = 'https://github.com/ggml-org/llama.cpp.git' KIND = 'llama_cpp_bf16_v1' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' +NATIVE_SESSION = 'connection_bound_v1' LEGACY_WRAPPERS = ('CMakeLists.txt', 'adapter.h', 'adapter.cpp', 'bounded-tensor-validation.patch') CONSTRAINED_WRAPPERS = LEGACY_WRAPPERS + ('output-constraint.h', 'output-constraint.cpp') +SESSION_WRAPPERS = CONSTRAINED_WRAPPERS + ('session-state.h',) MODEL = 'qwen3-4b-instruct-2507-v1' BASE = Path('/home/vpci/opencode-trial') CORE = Path('/home/vpci/source') @@ -144,10 +146,12 @@ def validate_build(core, base): tree = builder.verify_source(base / 'llama-source') value, report_hash = document(base / 'llama-build/build.json', 65536) builder.native.validate_build_provenance(value, require) - require(type(value.get('version')) is int and value['version'] in (2, 3), 'native_build_version') - constrained = value['version'] == 3 + require(type(value.get('version')) is int and value['version'] in (2, 3, 4), 'native_build_version') + constrained, session = value['version'] >= 3, value['version'] == 4 require(value.get('output_constraints') == [OUTPUT_CONSTRAINT] if constrained else 'output_constraints' not in value, 'native_build_constraint') + require(value.get('native_sessions') == [NATIVE_SESSION] if session + else 'native_sessions' not in value, 'native_build_session') require(value.get('kind') == KIND and value.get('source_commit') == SOURCE and value.get('source_tree') == tree and value.get('abi_version') == 1 and value.get('jobs') == 2 and value.get('cpu') == 'avx2_fma_f16c' @@ -157,8 +161,9 @@ def validate_build(core, base): 'native_build_provenance') library = file_identity(base / 'llama-build/libvolparossa_llama_cpu.so', 128 * 1024 ** 2) require(value.get('library') == {'path': 'libvolparossa_llama_cpu.so', **library}, 'native_library_binding') + names = SESSION_WRAPPERS if session else (CONSTRAINED_WRAPPERS if constrained else LEGACY_WRAPPERS) wrapper = {name: file_identity(core / 'workers/volparossa-ml/native-cpu' / name, 1024 ** 2) - for name in (CONSTRAINED_WRAPPERS if constrained else LEGACY_WRAPPERS)} + for name in names} require(value.get('wrapper_sources') == wrapper, 'native_wrapper_binding') builder.verify_loader_compilation(base / 'llama-build', base / 'llama-build/loader-overlay/llama-model-loader.cpp') @@ -169,7 +174,9 @@ def validate_build(core, base): library=library, wrapper_sources=wrapper, jobs=2, nice=19, cpu='avx2_fma_f16c', quantization=False, models_loaded=False) if constrained: - result.update(build_version=3, output_constraints=[OUTPUT_CONSTRAINT]) + result.update(build_version=value['version'], output_constraints=[OUTPUT_CONSTRAINT]) + if session: + result['native_sessions'] = [NATIVE_SESSION] return result @@ -186,21 +193,28 @@ def validate_provision(core, base, report, build): value, manifest_hash = document(root / 'backend.json', 16384) native = module(core / 'workers/volparossa-ml/llama_cpu.py', 'trial_native_contract') native.validate_manifest(value, require) - require(type(value.get('version')) is int and value['version'] in (1, 2), 'native_manifest_version') - constrained = value['version'] == 2 + require(type(value.get('version')) is int and value['version'] in (1, 2, 3), 'native_manifest_version') + constrained, session = value['version'] >= 2, value['version'] == 3 + expected_build = value['version'] + 1 require((value.get('output_constraints') == [OUTPUT_CONSTRAINT] - and build.get('build_version') == 3 and build.get('output_constraints') == [OUTPUT_CONSTRAINT]) + and build.get('build_version') == expected_build + and build.get('output_constraints') == [OUTPUT_CONSTRAINT]) if constrained else ('output_constraints' not in value and 'output_constraints' not in build and 'build_version' not in build), 'native_manifest_constraint') + require((value.get('native_sessions') == build.get('native_sessions') == [NATIVE_SESSION]) + if session else ('native_sessions' not in value and 'native_sessions' not in build), + 'native_manifest_session') require(manifest_hash == backend['backend_sha256'] and value['model_profile'] == MODEL and value['build_manifest_sha256'] == build['build_manifest_sha256'] and value['library'] == {'path': 'libvolparossa_llama_cpu.so', **build['library']}, 'native_manifest_binding') copied_build, build_hash = document(root / 'build.json', 65536) require(build_hash == build['build_manifest_sha256'], 'native_copied_build_binding') native.validate_build_provenance(copied_build, require) - require(type(copied_build.get('version')) is int and copied_build['version'] == (3 if constrained else 2) + require(type(copied_build.get('version')) is int and copied_build['version'] == expected_build and (copied_build.get('output_constraints') == [OUTPUT_CONSTRAINT] if constrained else 'output_constraints' not in copied_build), 'native_copied_build_constraint') + require(copied_build.get('native_sessions') == [NATIVE_SESSION] if session + else 'native_sessions' not in copied_build, 'native_copied_build_session') for name, maximum in (('library', 128 * 1024 ** 2), ('gguf', 9 * 1024 ** 3)): require(value[name] == {'path': value[name]['path'], **file_identity(root / value[name]['path'], maximum)}, 'native_artifact_binding') @@ -210,15 +224,23 @@ def validate_provision(core, base, report, build): gguf_sha256=value['gguf']['sha256'], gguf_bytes=value['gguf']['bytes'], source_weights_sha256=value['source_weights_sha256']) if constrained: - result.update(backend_version=2, build_version=3, output_constraints=[OUTPUT_CONSTRAINT]) + result.update(backend_version=value['version'], build_version=expected_build, + output_constraints=[OUTPUT_CONSTRAINT]) + if session: + result['native_sessions'] = [NATIVE_SESSION] return result def require_output_constraint(build, backend): - """Current explicit trial cannot silently run against legacy native artifacts.""" - require(build.get('build_version') == 3 and backend.get('build_version') == 3 - and backend.get('backend_version') == 2 - and build.get('output_constraints') == backend.get('output_constraints') == [OUTPUT_CONSTRAINT], + """Current trial requires both constrained output and the owner-session ABI. + + Historical validators above remain readable; neither an old constrained + build nor a session capability on only one artifact authorizes this trial. + """ + require(build.get('build_version') == 4 and backend.get('build_version') == 4 + and backend.get('backend_version') == 3 + and build.get('output_constraints') == backend.get('output_constraints') == [OUTPUT_CONSTRAINT] + and build.get('native_sessions') == backend.get('native_sessions') == [NATIVE_SESSION], 'native_output_constraint_required') diff --git a/scripts/opencode_session.cjs b/scripts/opencode_session.cjs index 51e70a7..62b44ff 100644 --- a/scripts/opencode_session.cjs +++ b/scripts/opencode_session.cjs @@ -48,6 +48,8 @@ async function runSession({input, output, events = process, outputConstraint}, h if (outputConstraint !== undefined && outputConstraint !== OUTPUT_CONSTRAINT) return 1; const spawnServer = hooks.spawn ?? spawn; let provider, child, exited, client, task, running, closing = false, bad = false, requested = false; + let nativeSession = false, providerClosing; + const closeProvider = () => providerClosing ??= provider.close(); let seq = 0, verificationSeq = 0, pendingApproval, pendingVerification, finish; const expiredApprovals = new Set(); const expiredVerifications = new Set(); @@ -117,10 +119,14 @@ async function runSession({input, output, events = process, outputConstraint}, h Buffer.byteLength(value.prompt) > 65536) { broken(); return; } requested = true; running = task.run(value.prompt, {signal: abort.signal, - ...(verification ? {verify, maxVerificationRounds: verification.maxRounds} : {})}).then(result => { + ...(verification ? {verify, maxVerificationRounds: verification.maxRounds} : {})}).then(async result => { + // One ordinary OpenCode task owns the warm model connection. Its final + // result is not released to the owner before private session state is gone. + if (nativeSession) await closeProvider(); if (!closing) send({type: 'result', result, diagnostics: provider?.diagnostics?.summary ?? null, task_diagnostics: task.diagnostics ?? null}); - }).catch(error => { + }).catch(async error => { + if (nativeSession) { try { await closeProvider(); } catch { bad = true; } } const reason = taskFailure(error); // A refused/incomplete/cancelled task is still a failed task, not proof of // failed runtime cleanup. OpenCodeTask has already awaited session deletion; @@ -149,9 +155,11 @@ async function runSession({input, output, events = process, outputConstraint}, h // Each provider request rechecks that identity; a core change cannot silently // switch the native task to another profile or provider. const model = caps.model_profile; + nativeSession = caps.inference_backend === 'llama_cpp_bf16_v1'; if (closing) throw Error('closed'); provider = await (hooks.provider ?? startChatCompletionsProvider)({socketPath: '/opt/core/compute.sock', model, - diagnostics: true, ...(outputConstraint !== undefined ? {outputConstraint} : {})}); + diagnostics: true, nativeSession, + ...(outputConstraint !== undefined ? {outputConstraint} : {})}); const password = randomBytes(32).toString('hex'); const cooperative = (hooks.cooperative ?? cooperativeMounted)(); const settings = runtimeSettings({baseUrl: provider.baseUrl, bearerToken: provider.bearerToken, password, cooperative, model}); @@ -186,7 +194,7 @@ async function runSession({input, output, events = process, outputConstraint}, h client?.close(); if (provider) { try { - await provider.close(); + await closeProvider(); if (provider.observations.submitted !== provider.observations.cleanup_confirmed) bad = true; } catch { bad = true; } } diff --git a/scripts/opencode_session.py b/scripts/opencode_session.py index 6044657..d0d605c 100644 --- a/scripts/opencode_session.py +++ b/scripts/opencode_session.py @@ -15,7 +15,7 @@ FIELDS = {'version', 'opencode', 'opencodeSha256', 'buildReport', 'node', 'nodeSha256', 'socketPath'} COOPERATIVE_FIELD = 'cooperativeSocketPath' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' -SOURCES = ('private-compute.cjs', 'private-conversation.cjs', 'responses-provider.cjs', +SOURCES = ('private-compute.cjs', 'private-conversation.cjs', 'private-native-session.cjs', 'responses-provider.cjs', 'chat-completions-provider.cjs', 'opencode-config.cjs', 'opencode-client.cjs', 'opencode-task.cjs', 'opencode-bridge.cjs') diff --git a/scripts/pack_opencode_cooperation.py b/scripts/pack_opencode_cooperation.py index fcd38af..d652625 100644 --- a/scripts/pack_opencode_cooperation.py +++ b/scripts/pack_opencode_cooperation.py @@ -21,7 +21,7 @@ NODE = (125989464, 'bc17c508ffeed0ec622934f9b7fa72f8e78da65350e63c3eceb56fa688aa5e12') NODE_LICENSE = (157606, '148eacf7863ef4329224a29398623077200a27194aa075569faf4a0a85566ca5') SOURCES = tuple('src/' + name for name in ( - 'private-compute.cjs', 'private-conversation.cjs', 'responses-provider.cjs', + 'private-compute.cjs', 'private-conversation.cjs', 'private-native-session.cjs', 'responses-provider.cjs', 'chat-completions-provider.cjs', 'opencode-config.cjs', 'opencode-client.cjs', 'opencode-task.cjs', 'opencode-bridge.cjs', 'opencode-runtime.cjs', 'cooperative-tool-client.cjs', 'cooperative-tool-server.cjs', diff --git a/src/chat-completions-provider.cjs b/src/chat-completions-provider.cjs index c248641..b475104 100644 --- a/src/chat-completions-provider.cjs +++ b/src/chat-completions-provider.cjs @@ -13,6 +13,7 @@ const { PrivateConversation, validateConversation, expectedLimits, check, keys, const { parseJson } = require('./responses-provider.cjs'); const { PROVIDER_ERRORS, emptyProviderDiagnostic } = require('./opencode-bridge.cjs'); const { terminalCleanupConfirmed } = require('./private-compute.cjs'); +const { PrivateNativeSession } = require('./private-native-session.cjs'); const HTTP_BYTES = 524288; const EXECUTION = Object.freeze({ scope: 'private_local', distributed: false, @@ -157,7 +158,8 @@ function toConversation(request, model, caps, outputConstraint) { } function completion(result, request) { - // PrivateConversation has already checked correlation, output bounds and terminal cleanup. + // The negotiated client checked correlation, output bounds and its versioned + // cleanup boundary. A v2 warm session still requires final owner-side close. if (!result.turn_complete) check(result.output.reason === 'token_limit', 'invalid_model_output'); const output = result.output; const isCall = output.type === 'function_call'; @@ -178,7 +180,8 @@ function completion(result, request) { choices: [{ index: 0, message, finish_reason: !result.turn_complete ? 'length' : isCall ? 'tool_calls' : 'stop' }], usage: { prompt_tokens: result.prompt_tokens, completion_tokens: result.generated_tokens, total_tokens: result.prompt_tokens + result.generated_tokens, - prompt_tokens_details: { cached_tokens: 0 }, completion_tokens_details: { reasoning_tokens: 0 } } }; + ...(result.version === 1 ? { prompt_tokens_details: { cached_tokens: 0 } } : {}), + completion_tokens_details: { reasoning_tokens: 0 } } }; } function streamChunks(answer, includeUsage) { @@ -216,15 +219,20 @@ async function readBody(request) { catch (error) { throw error.message?.startsWith('private_compute_') ? error : fail('invalid_json'); } } -async function startChatCompletionsProvider({ socketPath, model, diagnostics = false, outputConstraint }) { +async function startChatCompletionsProvider({ socketPath, model, diagnostics = false, outputConstraint, + nativeSession = false }) { expectedLimits(model); check(outputConstraint === undefined || outputConstraint === OUTPUT_CONSTRAINT && model === 'qwen3-4b-instruct-2507-v1', 'unsupported_output_constraint'); check(typeof diagnostics === 'boolean', 'diagnostic_scope'); + check(typeof nativeSession === 'boolean' && (!nativeSession || model === 'qwen3-4b-instruct-2507-v1'), + 'incompatible_capabilities'); new PrivateConversation(socketPath); // Path validation only; launch does not start compute. const bearerToken = randomBytes(32).toString('base64url'); const authorization = Buffer.from(`Bearer ${bearerToken}`); let host, active = null, closing = false, closingPromise; + let warm = null, taskDeadline = null, nativeFailure = false; + const warmOwners = new Set(); const observed = { submitted: 0, completed: 0, incomplete: 0, cleanup_confirmed: 0 }; const records = []; const summary = emptyProviderDiagnostic(); @@ -233,6 +241,13 @@ async function startChatCompletionsProvider({ socketPath, model, diagnostics = f if (object[key] < 65535) object[key]++; else summary.truncated = true; }; + const creditCleanup = owner => { + if (!owner?.client.sessionCleanupConfirmed) return; + while (owner.credited < owner.submitted) { + owner.credited++; observed.cleanup_confirmed++; + if (diagnostics) count(summary, 'cleanup_confirmed'); + } + }; const server = http.createServer({ maxHeaderSize: 8192, headersTimeout: 5000, requestTimeout: 10000, keepAliveTimeout: 1000 }, (request, response) => { const received = Buffer.from(request.headers.authorization ?? ''); @@ -246,11 +261,23 @@ async function startChatCompletionsProvider({ socketPath, model, diagnostics = f errorReply(response, 400, 'unsupported_request'); return; } if (closing || active) { errorReply(response, 503, 'busy'); return; } + if (nativeFailure) { errorReply(response, 422, 'execution_failed'); return; } + if (nativeSession && observed.submitted >= 128) { + errorReply(response, 422, 'execution_budget_exceeded'); return; + } if (Number(request.headers['content-length'] ?? 0) > HTTP_BYTES) { errorReply(response, 413, 'request_bound'); return; } const controller = new AbortController(); - const client = new PrivateConversation(socketPath, { generationPolicyVersion: 1, executionErrorVersion: 1, - ...(outputConstraint !== undefined ? { outputConstraintVersion: 1 } : {}) }); - const owner = { controller, client, done: null }; + const clientOptions = { generationPolicyVersion: 1, executionErrorVersion: 1, + ...(outputConstraint !== undefined ? { outputConstraintVersion: 1 } : {}) }; + if (nativeSession && warm?.client.sessionCleanupConfirmed) { + creditCleanup(warm); warm = null; + } + if (nativeSession && !warm) { + warm = {client: new PrivateNativeSession(socketPath, clientOptions), submitted: 0, credited: 0}; + warmOwners.add(warm); + } + const client = nativeSession ? warm.client : new PrivateConversation(socketPath, clientOptions); + const owner = { controller, client, warm: nativeSession ? warm : null, done: null, taskTimer: null }; active = owner; response.once('close', () => { if (!response.writableFinished) controller.abort(); }); request.once('aborted', () => controller.abort()); @@ -258,17 +285,24 @@ async function startChatCompletionsProvider({ socketPath, model, diagnostics = f try { const requestBody = await readBody(request); if (controller.signal.aborted) throw fail('cancelled'); - const caps = await client.connect(); + if (nativeSession) { + taskDeadline ??= performance.now() + 2400000; + const remaining = Math.floor(taskDeadline - performance.now()); + check(remaining > 0, 'execution_budget_exceeded'); + owner.taskTimer = setTimeout(() => controller.abort(), remaining); + } + const caps = nativeSession && client.sessionState === 'new' ? await client.open() : await client.connect(); const conversation = toConversation(requestBody, model, caps, outputConstraint); if (controller.signal.aborted) throw fail('cancelled'); observed.submitted++; + if (owner.warm) owner.warm.submitted++; if (diagnostics) count(summary, 'submitted'); const started = performance.now(); const result = await client.submit(conversation, { signal: controller.signal }); - observed.cleanup_confirmed++; + if (!nativeSession) observed.cleanup_confirmed++; if (result.turn_complete) observed.completed++; else observed.incomplete++; if (diagnostics) { - count(summary, 'cleanup_confirmed'); + if (!nativeSession) count(summary, 'cleanup_confirmed'); count(summary, result.turn_complete ? 'completed' : 'incomplete'); count(summary.results, result.output.type); if (!result.turn_complete) count(summary.incomplete_reasons, result.output.reason); @@ -284,12 +318,14 @@ async function startChatCompletionsProvider({ socketPath, model, diagnostics = f response.writeHead(200, { 'Content-Type': streaming ? 'text/event-stream' : 'application/json', 'Cache-Control': 'no-store', 'Connection': 'close', 'X-Content-Type-Options': 'nosniff', 'X-Volparossa-Execution': 'private-local', 'X-Volparossa-Confidential-Peer': 'unavailable' }); - // This is SSE protocol adaptation, not token-streaming model execution. No model text - // or tool proposal is released before the core confirms worker cleanup. + // SSE adaptation, not model token streaming. Legacy results require + // worker cleanup. Native-session v2 requires request-file cleanup while + // private KV remains in this task's worker until verified session close. response.end(streaming ? streamChunks(answer, requestBody.stream_options?.include_usage === true) .map(chunk => `data: ${JSON.stringify(chunk)}\n\n`).join('') + 'data: [DONE]\n\n' : JSON.stringify(answer)); } catch (error) { - if (terminalCleanupConfirmed(error)) { + if (nativeSession) nativeFailure = true; + if (!nativeSession && terminalCleanupConfirmed(error)) { observed.cleanup_confirmed++; if (diagnostics) count(summary, 'cleanup_confirmed'); } @@ -300,12 +336,24 @@ async function startChatCompletionsProvider({ socketPath, model, diagnostics = f // without a model result; it must not restart the same expensive turn. // OpenCode v1.18.34 retries every 5xx, so 502 would repeatedly regenerate // the same unusable greedy turn. Preserve transient/uncertain failures. - const status = ['busy', 'cleanup_unconfirmed', 'socket_unavailable', 'execution_failed'].includes(code) ? 503 : + const status = nativeSession && ['cleanup_unconfirmed', 'socket_unavailable', 'socket_error', + 'disconnected', 'execution_failed', 'execution_budget_exceeded'].includes(code) ? 422 : + ['busy', 'cleanup_unconfirmed', 'socket_unavailable', 'execution_failed'].includes(code) ? 503 : ['invalid_model_output', 'tool_choice_not_met', 'execution_budget_exceeded'].includes(code) ? 422 : code === 'request_bound' ? 413 : 400; errorReply(response, status, code); } finally { - client.close(); + clearTimeout(owner.taskTimer); + if (nativeSession) { + if (controller.signal.aborted) nativeFailure = true; + if (controller.signal.aborted && client.sessionState === 'open' && !client.pending) { + // Cancellation also ends an idle warm worker, not just an admitted + // request. Keep uncertainty visible to the final owner close. + try { await client.finish(); } catch { client.close(); } + } + creditCleanup(owner.warm); + } + else client.close(); if (active === owner) active = null; } })(); @@ -330,7 +378,23 @@ async function startChatCompletionsProvider({ socketPath, model, diagnostics = f owner?.controller.abort(); server.closeAllConnections(); await owner?.done; + let cleanupError; + if (nativeSession) { + try { + for (const session of warmOwners) { + // A failed handshake started no session. Once open was sent, an + // absent receipt is uncertainty even if no model turn was sent. + if (session.client.openId !== null) await session.client.finish(); + creditCleanup(session); + } + } catch (error) { cleanupError = error; } + finally { + for (const session of warmOwners) session.client.close(); + warmOwners.clear(); + } + } await new Promise(resolve => server.close(resolve)); + if (cleanupError) throw cleanupError; })(); return closingPromise; } }; diff --git a/src/private-conversation.cjs b/src/private-conversation.cjs index 269ee6e..338e163 100644 --- a/src/private-conversation.cjs +++ b/src/private-conversation.cjs @@ -159,7 +159,7 @@ function validateCall(item, tools, seen) { check(!seen.has(item.call_id) && tools.get(toolKey(item)) === (custom ? 'custom' : 'function')); if (custom) text(item.input, 4096); else payload(item.arguments); } -function validateResult(value, caps, input) { +function validateConversationResult(value, caps, input, version) { keys(value, ['version', 'operation', 'model_profile', 'execution_complete', 'turn_complete', 'output', 'prompt_tokens', 'generated_tokens', 'limits', 'local_only', 'private_data_supported', 'tool_execution', 'distributed_execution_claimed', 'private_training_claimed', 'model_answer_correctness_proven', 'cleanup'], @@ -173,10 +173,19 @@ function validateResult(value, caps, input) { validateConversation(input, caps); check(value.output_constraint === input.output_constraint, 'output_constraint_mismatch'); } - keys(value.cleanup, ['complete', 'retained_input', 'retained_report']); - check(value.cleanup.complete === true && value.cleanup.retained_input === false && - value.cleanup.retained_report === false, 'cleanup_unconfirmed'); - check(value.version === 1 && value.operation === 'compute_private_conversation' && + if (version === 1) { + keys(value.cleanup, ['complete', 'retained_input', 'retained_report']); + check(value.cleanup.complete === true && value.cleanup.retained_input === false && + value.cleanup.retained_report === false, 'cleanup_unconfirmed'); + } else { + // Request-file removal is deliberately NOT a claim that the warm worker or + // its private KV state has gone. A separate joined session-close is required. + keys(value.cleanup, ['version', 'request_files_removed', 'session_closed', 'session_private_state_retained']); + check(value.cleanup.version === 2 && value.cleanup.request_files_removed === true && + value.cleanup.session_closed === false && value.cleanup.session_private_state_retained === true, + 'cleanup_unconfirmed'); + } + check(value.version === version && value.operation === 'compute_private_conversation' && value.model_profile === caps.model_profile && value.execution_complete === true && value.local_only === true && value.private_data_supported === true && value.tool_execution === false && value.distributed_execution_claimed === false && value.private_training_claimed === false && @@ -200,6 +209,13 @@ function validateResult(value, caps, input) { return value; } +function validateResult(value, caps, input) { return validateConversationResult(value, caps, input, 1); } +function validateNativeSessionResult(value, caps, input) { + check(caps.native_session_version === 1 && caps.inference_backend === 'llama_cpp_bf16_v1', + 'incompatible_capabilities'); + return validateConversationResult(value, caps, input, 2); +} + class PrivateConversation extends PrivateCompute { constructor(socketPath, { generationPolicyVersion, executionErrorVersion, outputConstraintVersion } = {}) { super(socketPath); @@ -273,5 +289,6 @@ class PrivateConversation extends PrivateCompute { } } -module.exports = { PrivateConversation, validateConversation, validateResult, expectedLimits, requestLimit, capabilities, +module.exports = { PrivateConversation, validateConversation, validateResult, validateNativeSessionResult, + expectedLimits, requestLimit, capabilities, OUTPUT_CONSTRAINT, check, keys, text, identifier, object, fail }; diff --git a/src/private-native-session.cjs b/src/private-native-session.cjs new file mode 100644 index 0000000..6e09b96 --- /dev/null +++ b/src/private-native-session.cjs @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-3.0-only +'use strict'; +// Same-owner, single-connection native model reuse. No session token, shared KV, +// reconnect replay or remote-confidential-execution claim. +const { PrivateConversation, capabilities, validateNativeSessionResult, check, keys, object, fail } = + require('./private-conversation.cjs'); + +const SESSION_LIMITS = Object.freeze({version: 1, max_task_seconds: 2400, + max_idle_seconds: 300, max_requests: 128}); +function sessionCapabilities(value, outputConstraintVersion) { + check(object(value), 'incompatible_capabilities'); + const {native_session_version, native_session, ...ordinary} = value; + const caps = capabilities(ordinary, 1, 1, outputConstraintVersion); + check(native_session_version === 1 && caps.model_profile === 'qwen3-4b-instruct-2507-v1' && + caps.inference_backend === 'llama_cpp_bf16_v1', 'incompatible_capabilities'); + keys(native_session, Object.keys(SESSION_LIMITS)); + check(Object.entries(SESSION_LIMITS).every(([key, expected]) => native_session[key] === expected), + 'incompatible_capabilities'); + return Object.freeze({...caps, native_session_version: 1, + native_session: Object.freeze({...native_session})}); +} +function closedCleanup(value) { + keys(value, ['version', 'child_reaped', 'private_state_removed']); + check(value.version === 1 && value.child_reaped === true && value.private_state_removed === true, + 'cleanup_unconfirmed'); +} + +class PrivateNativeSession extends PrivateConversation { + constructor(socketPath, {outputConstraintVersion} = {}) { + super(socketPath, {generationPolicyVersion: 1, executionErrorVersion: 1, outputConstraintVersion}); + this.sessionState = 'new'; + this.sessionControl = null; + this.openId = null; + this.sessionCleanupConfirmed = false; + this.requests = 0; + this.finishPromise = null; + } + _send(id, operation) { + if (operation.type === 'capabilities') operation = {type: 'conversation_capabilities', + generation_policy_version: 1, execution_error_version: 1, native_session_version: 1, + ...(this.outputConstraintVersion === 1 ? {output_constraint_version: 1} : {})}; + super._send(id, operation); + } + async open() { + const caps = await this.connect(); + check(this.sessionState === 'new' && !this.sessionControl, 'busy'); + check(!caps.quarantined, 'cleanup_unconfirmed'); + this.sessionState = 'opening'; + const id = this._id(); this.openId = id; + await new Promise((resolve, reject) => { + this.sessionControl = {id, resolve, reject, timer: setTimeout(() => + this._fail(fail('cleanup_unconfirmed')), 15000)}; + this._send(id, {type: 'open_native_session'}); + }); + return caps; + } + async submit(conversation, options) { + check(this.sessionState === 'open', 'not_connected'); + check(this.requests < SESSION_LIMITS.max_requests, 'connection_request_limit'); + this.requests++; + return super.submit(conversation, options); + } + _response(message) { + check(object(message) && message.version === 1 && + typeof message.id === 'string' && /^[0-9a-f]{32}$/.test(message.id), 'invalid_response'); + if (message.event === 'conversation_capabilities') { + keys(message, ['version', 'id', 'event', 'capabilities']); + check(this.state === 'connecting' && message.id === this.handshake?.id, 'invalid_response'); + this.caps = sessionCapabilities(message.capabilities, this.outputConstraintVersion); + this.state = 'open'; clearTimeout(this.handshake.timer); + this.handshake.resolve(this.caps); this.handshake = null; return; + } + if (message.event === 'native_session_opened') { + keys(message, ['version', 'id', 'event']); + check(this.sessionState === 'opening' && message.id === this.sessionControl?.id, 'invalid_response'); + this.sessionState = 'open'; this._controlDone(); return; + } + if (message.event === 'native_session_closed') { + keys(message, ['version', 'id', 'event', 'cleanup']); + check(!this.pending && (this.sessionState === 'closing' && message.id === this.sessionControl?.id || + this.sessionState === 'open' && message.id === this.openId), 'invalid_response'); + closedCleanup(message.cleanup); + this.sessionCleanupConfirmed = true; this.sessionState = 'closed'; + this._controlDone(); + // The checked receipt, not EOF or a cancellation acknowledgement, proves + // cleanup. Keep that fact even if the socket closes immediately afterwards. + this.close(); return; + } + if (message.event === 'result') { + keys(message, ['version', 'id', 'event', 'result']); + check(this.sessionState === 'open' && this.pending?.admitted && message.id === this.pending.id, + 'invalid_response'); + const result = validateNativeSessionResult(message.result, this.caps, this.pending.input); + this._settle(this.pending.cancelled ? fail('cancelled') : null, result); return; + } + if (message.event === 'error' && this.sessionState !== 'new') { + if (this.cancels.has(message.id) && message.code === 'no_such_task') { + super._response(message); return; + } + if (this.sessionState === 'opening' && !Object.hasOwn(message, 'session_cleanup')) { + keys(message, ['version', 'id', 'event', 'code']); + check(message.id === this.sessionControl?.id && + ['invalid_request', 'handshake_required', 'busy'].includes(message.code), 'invalid_response'); + throw fail(message.code); + } + keys(message, ['version', 'id', 'event', 'code', 'session_cleanup']); + const correlated = message.id === this.pending?.id || message.id === this.sessionControl?.id || + this.sessionState === 'open' && !this.pending && message.id === this.openId; + check(correlated && + ['cancelled', 'execution_failed', 'execution_budget_exceeded', 'cleanup_unconfirmed'].includes(message.code), + 'invalid_response'); + if (message.session_cleanup === null || message.code === 'cleanup_unconfirmed') throw fail('cleanup_unconfirmed'); + closedCleanup(message.session_cleanup); + this.sessionCleanupConfirmed = true; this.sessionState = 'closed'; + const error = fail(message.code); + this._controlDone(error); this._settle(error); this.close(); return; + } + // Reuse framed transport, admitted and correlated cancel acknowledgements; + // no legacy result is ever passed to its all-state-removed validator. + super._response(message); + } + _settle(error, value) { + // Unlike legacy results, a warm result cannot stamp terminal cleanup onto + // an AbortError. Final cleanup is independently observable on this instance. + const pending = this.pending; this.pending = null; + if (!pending) return; + clearTimeout(pending.timer); pending.signal?.removeEventListener('abort', pending.abort); + if (error) pending.reject(error); else pending.resolve(value); + } + _controlDone(error) { + const control = this.sessionControl; this.sessionControl = null; + if (!control) return; + clearTimeout(control.timer); + if (error) control.reject(error); else control.resolve(); + } + _fail(error) { + this._controlDone(error); + if (!this.sessionCleanupConfirmed) this.sessionState = 'failed'; + super._fail(error); + } + finish() { + if (this.finishPromise) return this.finishPromise; + this.finishPromise = (async () => { + if (this.sessionCleanupConfirmed) return; + check(this.sessionState === 'open' && this.state === 'open' && !this.pending, 'cleanup_unconfirmed'); + this.sessionState = 'closing'; + const id = this._id(); + await new Promise((resolve, reject) => { + this.sessionControl = {id, resolve, reject, timer: setTimeout(() => + this._fail(fail('cleanup_unconfirmed')), 15000)}; + this._send(id, {type: 'close_native_session'}); + }); + check(this.sessionCleanupConfirmed, 'cleanup_unconfirmed'); + })(); + return this.finishPromise; + } +} + +module.exports = {PrivateNativeSession, sessionCapabilities, closedCleanup, SESSION_LIMITS}; diff --git a/tests/opencode-session.test.cjs b/tests/opencode-session.test.cjs index a16325f..d705cd6 100644 --- a/tests/opencode-session.test.cjs +++ b/tests/opencode-session.test.cjs @@ -21,7 +21,8 @@ function fixture(Task, receive, options = {}) { const hooks = {Task, preflight: async () => options.capabilities ?? {...caps(options.model ?? DEFAULT_MODEL), generation_policy_version: 1, generation_policies: ['greedy_v1'], execution_error_version: 1}, - provider: async value => { binding.provider = value.model; binding.outputConstraint = value.outputConstraint; return provider; }, + provider: async value => { binding.provider = value.model; binding.outputConstraint = value.outputConstraint; + binding.nativeSession = value.nativeSession; return provider; }, prepare(cooperative) { assert.equal(cooperative, options.cooperative ?? false); }, cooperative: () => options.cooperative ?? false, port: async () => 1235, approvalMs: 15, spawn(binary, args, config) { @@ -61,6 +62,8 @@ test('explicit constraint is negotiated before runtime startup and bound to the const f = fixture(Task, (_value, input) => input.end(), {model, capabilities, outputConstraint: selected}); assert.equal(await f.done, 0); assert.equal(f.binding.outputConstraint, selected); + assert.equal(f.binding.nativeSession, true); + assert.deepEqual(f.observed, ['ready', 'provider-close', 'result', 'client-close', 'SIGTERM']); for (const output_constraints of [[], undefined]) { const rejected = fixture(Task, (_value, input) => input.end(), {model, outputConstraint: selected, capabilities: {...capabilities, output_constraints}}); diff --git a/tests/private-native-session.test.cjs b/tests/private-native-session.test.cjs new file mode 100644 index 0000000..845704a --- /dev/null +++ b/tests/private-native-session.test.cjs @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Framed socket/HTTP contracts with synthetic replies; no native inference proof. +'use strict'; +const assert = require('node:assert/strict'); +const {test} = require('node:test'); +const http = require('node:http'); +const {PrivateNativeSession, sessionCapabilities, SESSION_LIMITS} = require('../src/private-native-session.cjs'); +const {validateResult} = require('../src/private-conversation.cjs'); +const {terminalCleanupConfirmed} = require('../src/private-compute.cjs'); +const {startChatCompletionsProvider} = require('../src/chat-completions-provider.cjs'); +const {caps, input, result, reply, fixture} = require('./conversation-fixture.cjs'); + +const MODEL = 'qwen3-4b-instruct-2507-v1'; +const CLEAN = Object.freeze({version: 1, child_reaped: true, private_state_removed: true}); +function nativeCaps() { return {...caps(MODEL), generation_policy_version: 1, generation_policies: ['greedy_v1'], + execution_error_version: 1, inference_backend: 'llama_cpp_bf16_v1', required_generation_policy: 'greedy_v1', + native_session_version: 1, native_session: {...SESSION_LIMITS}}; } +function conversation() { return {...input(), generation_policy: 'greedy_v1'}; } +function answer() { return {...result(undefined, MODEL), version: 2, generation_policy: 'greedy_v1', + cleanup: {version: 2, request_files_removed: true, session_closed: false, session_private_state_retained: true}}; } +async function setup(t, handler, capabilities = nativeCaps()) { + const f = await fixture(t, (socket, request) => { + if (request.operation.type === 'open_native_session') reply(socket, request, 'native_session_opened'); + else handler(socket, request); + }, capabilities); + const session = new PrivateNativeSession(f.socketPath); + t.after(() => session.close()); + return {...f, session}; +} +function closeReply(socket, request) { + reply(socket, request, 'native_session_closed', {cleanup: CLEAN}); socket.end(); +} +function generate(socket, request) { + reply(socket, request, 'admitted'); reply(socket, request, 'result', {result: answer()}); +} +test('one connection reuses two complete ordinary conversations and proves cleanup only on joined close', async t => { + const f = await setup(t, (socket, request) => { + if (request.operation.type === 'close_native_session') closeReply(socket, request); + else generate(socket, request); + }); + await f.session.open(); + const original = conversation(); + const first = await f.session.submit(original), second = await f.session.submit(original); + assert.equal(first.version, 2); assert.equal(second.version, 2); + assert.equal(f.session.sessionCleanupConfirmed, false); + assert.throws(() => validateResult(first, caps(MODEL), original)); + assert.deepEqual(f.requests[0].operation, {type: 'conversation_capabilities', generation_policy_version: 1, + execution_error_version: 1, native_session_version: 1}); + assert.deepEqual(f.requests.slice(2).map(row => row.operation.conversation), [original, original]); + await f.session.finish(); + assert.equal(f.session.sessionCleanupConfirmed, true); + assert.deepEqual(f.requests.map(row => row.operation.type), ['conversation_capabilities', + 'open_native_session', 'submit_conversation', 'submit_conversation', 'close_native_session']); + await f.session.finish(); + await assert.rejects(f.session.submit(original), /not_connected/); +}); +test('native capabilities are closed and cannot silently accept old core, widened limits or wrong backend', () => { + for (const changed of [caps(MODEL), {...nativeCaps(), native_session_version: 2}, + {...nativeCaps(), native_session: {...SESSION_LIMITS, max_task_seconds: 2401}}, + {...nativeCaps(), native_session: {...SESSION_LIMITS, token: 'not-authority'}}, + {...nativeCaps(), inference_backend: 'remote_unverified'}]) { + assert.throws(() => sessionCapabilities(changed)); + } + assert.ok(Object.isFrozen(sessionCapabilities(nativeCaps()).native_session)); +}); +test('legacy or falsely cleaned results cannot release a warm model answer', async t => { + for (const changed of [result(undefined, MODEL), {...answer(), cleanup: {...answer().cleanup, session_closed: true}}, + {...answer(), cleanup: {...answer().cleanup, request_files_removed: false}}]) { + const f = await setup(t, (socket, request) => { + reply(socket, request, 'admitted'); reply(socket, request, 'result', {result: changed}); + }); + await f.session.open(); + await assert.rejects(f.session.submit(conversation())); + assert.equal(f.session.sessionCleanupConfirmed, false); + await assert.rejects(f.session.finish(), /cleanup_unconfirmed/); + } +}); +test('request cancellation awaits whole-session cleanup, not the cancellation acknowledgement', async t => { + let admitted, complete, submit; + const ready = new Promise(resolve => { admitted = resolve; }); + const f = await setup(t, (socket, request) => { + if (request.operation.type === 'submit_conversation') { + submit = request; reply(socket, request, 'admitted'); admitted(); + } else if (request.operation.type === 'cancel') { + reply(socket, request, 'cancel_requested', {task_id: submit.id}); + complete = () => { reply(socket, submit, 'error', {code: 'cancelled', session_cleanup: CLEAN}); socket.end(); }; + } else assert.fail('no retry or extra operation'); + }); + await f.session.open(); + const controller = new AbortController(); + const pending = f.session.submit(conversation(), {signal: controller.signal}); + const rejected = assert.rejects(pending, error => error.code === 'cancelled' && !terminalCleanupConfirmed(error)); + await ready; controller.abort(); + while (!complete) await new Promise(resolve => setImmediate(resolve)); + assert.equal(f.session.sessionCleanupConfirmed, false); + complete(); await rejected; await f.session.finish(); + assert.equal(f.session.sessionCleanupConfirmed, true); +}); +test('missing or incorrect session cleanup remains uncertain even after a valid warm answer', async t => { + for (const cleanup of [null, {...CLEAN, child_reaped: false}, {...CLEAN, private_state_removed: false}]) { + const f = await setup(t, (socket, request) => { + if (request.operation.type === 'submit_conversation') generate(socket, request); + else { reply(socket, request, 'native_session_closed', {cleanup}); socket.end(); } + }); + await f.session.open(); await f.session.submit(conversation()); + await assert.rejects(f.session.finish()); + assert.equal(f.session.sessionCleanupConfirmed, false); + } +}); +test('bare disconnect is not a cleanup receipt and cannot reconnect this session', async t => { + let socket; + const f = await setup(t, (peer, request) => { socket = peer; generate(peer, request); }); + await f.session.open(); await f.session.submit(conversation()); + socket.destroy(); + while (f.session.state !== 'closed') await new Promise(resolve => setImmediate(resolve)); + assert.equal(f.session.sessionCleanupConfirmed, false); + await assert.rejects(f.session.finish()); await assert.rejects(f.session.open()); +}); +test('foreign close receipt and unknown error fields cannot claim cleanup', async t => { + const f = await setup(t, (socket, request) => { + reply(socket, {...request, id: 'ab'.repeat(16)}, 'native_session_closed', {cleanup: CLEAN}); + }); + await f.session.open(); + await assert.rejects(f.session.finish()); + assert.equal(f.session.sessionCleanupConfirmed, false); +}); +test('opening failure accepts only correlated real cleanup, not a bare error or EOF', async t => { + for (const cleanup of [CLEAN, null]) { + const f = await fixture(t, (socket, request) => { + assert.equal(request.operation.type, 'open_native_session'); + reply(socket, request, 'error', {code: cleanup ? 'execution_failed' : 'cleanup_unconfirmed', + session_cleanup: cleanup}); socket.end(); + }, nativeCaps()); + const session = new PrivateNativeSession(f.socketPath); + t.after(() => session.close()); + await assert.rejects(session.open()); + assert.equal(session.sessionCleanupConfirmed, cleanup !== null); + if (cleanup) await session.finish(); else await assert.rejects(session.finish()); + } +}); +test('failed explicit close preserves a proven cleanup receipt but still rejects the operation', async t => { + const f = await setup(t, (socket, request) => { + reply(socket, request, 'error', {code: 'execution_failed', session_cleanup: CLEAN}); socket.end(); + }); + await f.session.open(); + await assert.rejects(f.session.finish(), /execution_failed/); + assert.equal(f.session.sessionCleanupConfirmed, true); +}); + +function send(provider, prompt) { + const body = JSON.stringify({model: MODEL, temperature: 0, messages: [{role: 'user', content: prompt}]}); + return new Promise((resolve, reject) => { + const request = http.request(provider.baseUrl + '/chat/completions', {method: 'POST', headers: { + authorization: `Bearer ${provider.bearerToken}`, 'content-type': 'application/json', + 'content-length': Buffer.byteLength(body)}}, response => { + let value = ''; response.on('data', chunk => { value += chunk; }); + response.on('end', () => resolve({status: response.statusCode, body: JSON.parse(value)})); + }); + request.on('error', reject); request.end(body); + }); +} +test('normal HTTP provider preserves context and delays full cleanup counters until session close', async t => { + const f = await setup(t, (socket, request) => { + if (request.operation.type === 'close_native_session') closeReply(socket, request); + else generate(socket, request); + }); + const provider = await startChatCompletionsProvider({socketPath: f.socketPath, model: MODEL, + nativeSession: true, diagnostics: true}); + t.after(() => provider.close()); + for (const prompt of ['First private input.', 'Second private input.']) { + const response = await send(provider, prompt); + assert.equal(response.status, 200); + assert.equal(response.body.usage.prompt_tokens_details, undefined); + } + assert.deepEqual(provider.observations, {submitted: 2, completed: 2, incomplete: 0, cleanup_confirmed: 0}); + assert.equal(f.requests.filter(row => row.operation.type === 'open_native_session').length, 1); + assert.deepEqual(f.requests.filter(row => row.operation.type === 'submit_conversation') + .map(row => row.operation.conversation.history[0].text), ['First private input.', 'Second private input.']); + assert.ok(!JSON.stringify(provider.diagnostics).includes('private input')); + await provider.close(); + assert.equal(provider.observations.cleanup_confirmed, 2); + assert.equal(provider.diagnostics.summary.cleanup_confirmed, 2); +}); +test('a cleanup-confirmed failed native turn is terminal HTTP422 and cannot replay in a new session', async t => { + const f = await setup(t, (socket, request) => { + reply(socket, request, 'admitted'); + reply(socket, request, 'error', {code: 'execution_failed', session_cleanup: CLEAN}); socket.end(); + }); + const provider = await startChatCompletionsProvider({socketPath: f.socketPath, model: MODEL, + nativeSession: true, diagnostics: true}); + t.after(() => provider.close()); + assert.equal((await send(provider, 'Original task.')).status, 422); + assert.equal((await send(provider, 'Original task.')).status, 422); + assert.equal(f.requests.filter(row => row.operation.type === 'submit_conversation').length, 1); + assert.equal(f.requests.filter(row => row.operation.type === 'open_native_session').length, 1); + await provider.close(); + assert.deepEqual(provider.observations, {submitted: 1, completed: 0, incomplete: 0, cleanup_confirmed: 1}); +}); +test('verified idle closure permits a new request, but carries neither tokens nor old context across connections', async t => { + let firstSocket, firstOpen; + const f = await fixture(t, (socket, request) => { + if (request.operation.type === 'open_native_session') { + firstSocket ??= socket; firstOpen ??= request; + reply(socket, request, 'native_session_opened'); + } else if (request.operation.type === 'close_native_session') closeReply(socket, request); + else generate(socket, request); + }, nativeCaps()); + const provider = await startChatCompletionsProvider({socketPath: f.socketPath, model: MODEL, nativeSession: true}); + t.after(() => provider.close()); + assert.equal((await send(provider, 'Before idle close.')).status, 200); + closeReply(firstSocket, firstOpen); + await new Promise(resolve => firstSocket.once('close', resolve)); + assert.equal((await send(provider, 'After idle close.')).status, 200); + assert.equal(f.requests.filter(row => row.operation.type === 'open_native_session').length, 2); + await provider.close(); + assert.equal(provider.observations.cleanup_confirmed, 2); +}); diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index cc7ed24..da45377 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -691,7 +691,8 @@ def fail(native, listener, nonce, deadline, limit, observed): TRIAL.run_native_build(11 * TRIAL.GIB, {}) self.assertEqual(start.call_count, 1) - def bundle_fixture(self, directory, constrained=False): + def bundle_fixture(self, directory, constrained=False, session=False): + constrained = constrained or session base, core = Path(directory) / 'base', Path(directory) / 'core' build_root, backend = base / 'llama-build', base / 'ml/native-backend' wrappers = core / 'workers/volparossa-ml/native-cpu' @@ -702,23 +703,30 @@ def artifact(path, content): return {'bytes': len(content), 'sha256': hashlib.sha256(content).hexdigest()} library = artifact(build_root / 'libvolparossa_llama_cpu.so', b'inert library bytes, never loaded') (backend / 'libvolparossa_llama_cpu.so').write_bytes((build_root / 'libvolparossa_llama_cpu.so').read_bytes()) - wrapper = {name: artifact(wrappers / name, b'inert source ' + name.encode()) - for name in (NATIVE.CONSTRAINED_WRAPPERS if constrained else NATIVE.LEGACY_WRAPPERS)} - raw_build = dict(version=3 if constrained else 2, kind=KIND, source_commit=NATIVE.SOURCE, source_tree='b' * 40, + names = NATIVE.SESSION_WRAPPERS if session else ( + NATIVE.CONSTRAINED_WRAPPERS if constrained else NATIVE.LEGACY_WRAPPERS) + wrapper = {name: artifact(wrappers / name, b'inert source ' + name.encode()) for name in names} + raw_build = dict(version=4 if session else (3 if constrained else 2), + kind=KIND, source_commit=NATIVE.SOURCE, source_tree='b' * 40, abi_version=1, jobs=2, cpu='avx2_fma_f16c', models_loaded=False, downloads=False, install=False, sanitizers=False, provisionable=True, quantization=False, library={'path': 'libvolparossa_llama_cpu.so', **library}, wrapper_sources=wrapper, dynamic_dependencies=['libc.so.6']) if constrained: raw_build['output_constraints'] = [NATIVE.OUTPUT_CONSTRAINT] + if session: + raw_build['native_sessions'] = [NATIVE.NATIVE_SESSION] for root in (build_root, backend): (root / 'build.json').write_text(json.dumps(raw_build)) build_hash = hashlib.sha256((build_root / 'build.json').read_bytes()).hexdigest() gguf = artifact(backend / 'model.gguf', b'inert gguf bytes, never loaded') - manifest = dict(version=2 if constrained else 1, model_profile=NATIVE.MODEL, build_manifest_sha256=build_hash, + manifest = dict(version=3 if session else (2 if constrained else 1), + model_profile=NATIVE.MODEL, build_manifest_sha256=build_hash, library=raw_build['library'], gguf={'path': 'model.gguf', **gguf}, source_weights_sha256='c' * 64) if constrained: manifest['output_constraints'] = [NATIVE.OUTPUT_CONSTRAINT] + if session: + manifest['native_sessions'] = [NATIVE.NATIVE_SESSION] (backend / 'backend.json').write_text(json.dumps(manifest)) report = dict(native_cpu_converter={'implementation': KIND, 'dependencies': {'sentencepiece': '0.2.1'}}, native_backend={'kind': KIND, 'root': str(backend), @@ -769,8 +777,13 @@ def test_constrained_build3_backend2_preserve_closed_provenance_and_require_capa build = NATIVE.validate_build(core, base) with patch.object(NATIVE, 'module', return_value=parser): backend = NATIVE.validate_provision(core, base, report, build) - if selected: + # Historical artifacts remain readable, but this new actual + # requires a separately verified owner-session capability. + with self.assertRaisesRegex(ValueError, 'native_output_constraint_required'): NATIVE.require_output_constraint(build, backend) + self.assertNotIn('native_sessions', build) + self.assertNotIn('native_sessions', backend) + if selected: self.assertEqual(build['build_version'], 3) self.assertEqual(backend['build_version'], 3) self.assertEqual(backend['backend_version'], 2) @@ -778,8 +791,6 @@ def test_constrained_build3_backend2_preserve_closed_provenance_and_require_capa self.assertEqual(backend['output_constraints'], [NATIVE.OUTPUT_CONSTRAINT]) self.assertEqual(set(build['wrapper_sources']), set(NATIVE.CONSTRAINED_WRAPPERS)) else: - with self.assertRaisesRegex(ValueError, 'native_output_constraint_required'): - NATIVE.require_output_constraint(build, backend) self.assertNotIn('build_version', build) self.assertNotIn('output_constraints', build) self.assertNotIn('backend_version', backend) @@ -788,6 +799,127 @@ def test_constrained_build3_backend2_preserve_closed_provenance_and_require_capa self.assertNotIn('passed', backend) self.assertNotIn(str(base), json.dumps(backend)) + def test_session_build4_backend3_bind_seven_exact_sources_without_execution_claims(self): + with tempfile.TemporaryDirectory() as temp: + base, core, report, builder, parser = self.bundle_fixture(temp, session=True) + with patch.object(NATIVE, 'module', return_value=builder): + build = NATIVE.validate_build(core, base) + with patch.object(NATIVE, 'module', return_value=parser): + backend = NATIVE.validate_provision(core, base, report, build) + NATIVE.require_output_constraint(build, backend) + self.assertEqual((build['build_version'], backend['build_version'], backend['backend_version']), + (4, 4, 3)) + self.assertEqual(build['native_sessions'], [NATIVE.NATIVE_SESSION]) + self.assertEqual(backend['native_sessions'], [NATIVE.NATIVE_SESSION]) + self.assertEqual(set(build['wrapper_sources']), set(NATIVE.SESSION_WRAPPERS)) + self.assertEqual(set(NATIVE.SESSION_WRAPPERS) - set(NATIVE.CONSTRAINED_WRAPPERS), + {'session-state.h'}) + self.assertEqual(len(NATIVE.SESSION_WRAPPERS), 7) + self.assertFalse(build['models_loaded']) + self.assertEqual(build['jobs'], 2) + for token in ('passed', 'child_reaped', 'session_closed', str(base)): + self.assertNotIn(token, json.dumps(backend)) + + def test_session_build_capability_or_source_substitution_is_rejected(self): + for changes in ({'version': True}, {'version': 5}, {'version': 3}, + {'native_sessions': None}, {'native_sessions': []}, + {'native_sessions': ['unknown']}, + {'native_sessions': [NATIVE.NATIVE_SESSION, NATIVE.NATIVE_SESSION]}, + {'output_constraints': []}): + with self.subTest(changes=changes), tempfile.TemporaryDirectory() as temp: + base, core, _, builder, _ = self.bundle_fixture(temp, session=True) + path = base / 'llama-build/build.json' + path.write_text(json.dumps(json.loads(path.read_text()) | changes)) + with patch.object(NATIVE, 'module', return_value=builder), self.assertRaises(ValueError): + NATIVE.validate_build(core, base) + for mutation in ('missing', 'modified', 'unlisted', 'extra'): + with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as temp: + base, core, _, builder, _ = self.bundle_fixture(temp, session=True) + path = core / 'workers/volparossa-ml/native-cpu/session-state.h' + if mutation == 'missing': + path.unlink() + elif mutation == 'modified': + path.write_bytes(b'PRIVATE-CANARY changed source') + else: + manifest = base / 'llama-build/build.json' + value = json.loads(manifest.read_text()) + if mutation == 'unlisted': + del value['wrapper_sources']['session-state.h'] + else: + value['wrapper_sources']['private.c'] = value['wrapper_sources']['session-state.h'] + manifest.write_text(json.dumps(value)) + with patch.object(NATIVE, 'module', return_value=builder), self.assertRaises((OSError, ValueError)): + NATIVE.validate_build(core, base) + + def test_session_backend_rejects_missing_mixed_and_copied_capability(self): + for changes in ({'version': True}, {'version': 4}, {'version': 2}, + {'native_sessions': None}, {'native_sessions': []}, + {'native_sessions': ['unknown']}, {'output_constraints': []}): + with self.subTest(changes=changes), tempfile.TemporaryDirectory() as temp: + base, core, report, builder, parser = self.bundle_fixture(temp, session=True) + with patch.object(NATIVE, 'module', return_value=builder): + build = NATIVE.validate_build(core, base) + path = base / 'ml/native-backend/backend.json' + path.write_text(json.dumps(json.loads(path.read_text()) | changes)) + report['native_backend']['backend_sha256'] = hashlib.sha256(path.read_bytes()).hexdigest() + with patch.object(NATIVE, 'module', return_value=parser), self.assertRaises(ValueError): + NATIVE.validate_provision(core, base, report, build) + for changes in ({'version': 3}, {'native_sessions': []}): + with self.subTest(copied=changes), tempfile.TemporaryDirectory() as temp: + base, core, report, builder, parser = self.bundle_fixture(temp, session=True) + with patch.object(NATIVE, 'module', return_value=builder): + build = NATIVE.validate_build(core, base) + copied = base / 'ml/native-backend/build.json' + copied.write_text(json.dumps(json.loads(copied.read_text()) | changes)) + digest = hashlib.sha256(copied.read_bytes()).hexdigest() + build['build_manifest_sha256'] = digest + manifest = base / 'ml/native-backend/backend.json' + manifest.write_text(json.dumps(json.loads(manifest.read_text()) | {'build_manifest_sha256': digest})) + report['native_backend']['backend_sha256'] = hashlib.sha256(manifest.read_bytes()).hexdigest() + with patch.object(NATIVE, 'module', return_value=parser), self.assertRaises(ValueError): + NATIVE.validate_provision(core, base, report, build) + + def test_legacy_versions_reject_unsolicited_session_capability(self): + for constrained in (False, True): + for target in ('build', 'manifest', 'projection'): + with self.subTest(constrained=constrained, target=target), tempfile.TemporaryDirectory() as temp: + base, core, report, builder, parser = self.bundle_fixture(temp, constrained=constrained) + if target == 'build': + path = base / 'llama-build/build.json' + path.write_text(json.dumps(json.loads(path.read_text()) | + {'native_sessions': [NATIVE.NATIVE_SESSION]})) + with patch.object(NATIVE, 'module', return_value=builder), self.assertRaises(ValueError): + NATIVE.validate_build(core, base) + else: + with patch.object(NATIVE, 'module', return_value=builder): + build = NATIVE.validate_build(core, base) + if target == 'manifest': + path = base / 'ml/native-backend/backend.json' + path.write_text(json.dumps(json.loads(path.read_text()) | + {'native_sessions': [NATIVE.NATIVE_SESSION]})) + report['native_backend']['backend_sha256'] = hashlib.sha256(path.read_bytes()).hexdigest() + else: + build['native_sessions'] = [NATIVE.NATIVE_SESSION] + with patch.object(NATIVE, 'module', return_value=parser), self.assertRaises(ValueError): + NATIVE.validate_provision(core, base, report, build) + + def test_current_trial_requires_exact_session_on_both_closed_projections(self): + build = dict(build_version=4, output_constraints=[NATIVE.OUTPUT_CONSTRAINT], + native_sessions=[NATIVE.NATIVE_SESSION]) + backend = dict(build, backend_version=3) + NATIVE.require_output_constraint(build, backend) + for field in ('build_version', 'output_constraints', 'native_sessions'): + for side in ('build', 'backend'): + for value in (None, [], True, 'PRIVATE-CANARY'): + changed_build, changed_backend = copy.deepcopy(build), copy.deepcopy(backend) + (changed_build if side == 'build' else changed_backend)[field] = value + with self.subTest(field=field, side=side, value=value), \ + self.assertRaisesRegex(ValueError, 'native_output_constraint_required'): + NATIVE.require_output_constraint(changed_build, changed_backend) + for old in ({}, dict(backend, backend_version=2), dict(backend, build_version=3)): + with self.assertRaisesRegex(ValueError, 'native_output_constraint_required'): + NATIVE.require_output_constraint(build, old) + def test_constrained_build_cannot_omit_substitute_or_downgrade_wrapper_capability(self): changes = ({'version': True}, {'version': 4}, {'version': 2}, {'output_constraints': None}, {'output_constraints': []}, {'output_constraints': ['unknown']}, diff --git a/tests/test_pack_opencode_cooperation.py b/tests/test_pack_opencode_cooperation.py index 455781a..b36e37a 100644 --- a/tests/test_pack_opencode_cooperation.py +++ b/tests/test_pack_opencode_cooperation.py @@ -18,9 +18,9 @@ class CooperationCaptureTests(unittest.TestCase): def test_proposal_mode_has_exact_additive_node_only_inventory_and_module_closure(self): - self.assertEqual(len(PACK.SOURCES), 21) - self.assertEqual(len(PACK.PROPOSAL_SOURCES), 26) - self.assertEqual(len(set(PACK.PROPOSAL_SOURCES)), 26) + self.assertEqual(len(PACK.SOURCES), 22) + self.assertEqual(len(PACK.PROPOSAL_SOURCES), 27) + self.assertEqual(len(set(PACK.PROPOSAL_SOURCES)), 27) for name in PACK.PROPOSAL_SOURCES: if not name.endswith('.cjs'): continue @@ -57,7 +57,7 @@ def test_proposal_capture_has_no_opencode_binary_or_local_planner_and_requires_p manifest = json.loads((output / 'INPUTS.json').read_text()) self.assertEqual(set(manifest), {'version', 'kind', 'code_revision', 'node_version', 'files'}) self.assertEqual(manifest['kind'], 'public-code-proposal-inputs') - self.assertEqual(result['files'], 28) + self.assertEqual(result['files'], 29) self.assertIs(result['actual_runtime_execution'], False) self.assertEqual({name for name in manifest['files'] if name.startswith('runtime/')}, {'runtime/node', 'runtime/node-LICENSE'}) @@ -115,7 +115,7 @@ def test_exact_committed_sources_and_existing_binary_are_captured_not_executed(s selected.assert_called_once_with('b' * 40) manifest = json.loads((output / 'INPUTS.json').read_text()) self.assertEqual(result['manifest_sha256'], PACK.digest(output / 'INPUTS.json')) - self.assertEqual(result['files'], 25) + self.assertEqual(result['files'], 26) self.assertIs(result['actual_peer_execution'], False) for name, expected in manifest['files'].items(): actual = output / name From 266d1a03f0b327cc7221610372a8c626fe4b50d9 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:40:14 +0200 Subject: [PATCH 30/39] test: bind native session trial to verified core revision --- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 1745122..c730e8f 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = '3327115e07d2c4fa976b2d0c625729a68df0aaf6' +NATIVE_CORE = '0de65f423a6d96fa565facf6f283efae70adf2bd' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index da45377..6d8ff13 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '3327115e07d2c4fa976b2d0c625729a68df0aaf6') + self.assertEqual(TRIAL.NATIVE_CORE, '0de65f423a6d96fa565facf6f283efae70adf2bd') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From f23f9c90abe273ce07938b9f7cd5c16409ac52ca Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:49:19 +0200 Subject: [PATCH 31/39] test(code): pin closed native-session diagnostic core --- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index c730e8f..f74f531 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = '0de65f423a6d96fa565facf6f283efae70adf2bd' +NATIVE_CORE = 'a37f5cd0b1ec212632dcff95cac4e6da589027e9' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 6d8ff13..d01ce69 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '0de65f423a6d96fa565facf6f283efae70adf2bd') + self.assertEqual(TRIAL.NATIVE_CORE, 'a37f5cd0b1ec212632dcff95cac4e6da589027e9') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From a15a55243679cd6749ae7a1f2bab0919fbc05237 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:07:08 +0200 Subject: [PATCH 32/39] test(code): pin main-integrated native session core --- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index f74f531..08fbe46 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = 'a37f5cd0b1ec212632dcff95cac4e6da589027e9' +NATIVE_CORE = '01c90c574cf3e0e39a90aa7931a90b7c37811995' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index d01ce69..e4bc63b 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, 'a37f5cd0b1ec212632dcff95cac4e6da589027e9') + self.assertEqual(TRIAL.NATIVE_CORE, '01c90c574cf3e0e39a90aa7931a90b7c37811995') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From ffc7c3ef88aafd8ffc2c5652dbae5cf598ef9390 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:50:13 +0200 Subject: [PATCH 33/39] Bind native coding trial to bounded retained context capacity --- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 08fbe46..2b777de 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = '01c90c574cf3e0e39a90aa7931a90b7c37811995' +NATIVE_CORE = '95ef4541e518f9f9050dedc5d24bd4176b70db71' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index e4bc63b..7ca5f47 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '01c90c574cf3e0e39a90aa7931a90b7c37811995') + self.assertEqual(TRIAL.NATIVE_CORE, '95ef4541e518f9f9050dedc5d24bd4176b70db71') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 9da8608ed35318b28cf0a431a2581105a7b9b2b0 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:40:17 +0200 Subject: [PATCH 34/39] fix(code): bind aligned native CPU provenance and compiler checks --- scripts/opencode_native_cpu.py | 29 ++++- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 189 ++++++++++++++++++++++++++-- 3 files changed, 210 insertions(+), 10 deletions(-) diff --git a/scripts/opencode_native_cpu.py b/scripts/opencode_native_cpu.py index cd782d6..7cf2d32 100644 --- a/scripts/opencode_native_cpu.py +++ b/scripts/opencode_native_cpu.py @@ -28,6 +28,8 @@ LEGACY_WRAPPERS = ('CMakeLists.txt', 'adapter.h', 'adapter.cpp', 'bounded-tensor-validation.patch') CONSTRAINED_WRAPPERS = LEGACY_WRAPPERS + ('output-constraint.h', 'output-constraint.cpp') SESSION_WRAPPERS = CONSTRAINED_WRAPPERS + ('session-state.h',) +ALIGNED_WRAPPERS = SESSION_WRAPPERS + ('q1-aligned-load.patch',) +CPU_FIELDS = ('cpu_source_overlay', 'cpu_compile_source_verified') MODEL = 'qwen3-4b-instruct-2507-v1' BASE = Path('/home/vpci/opencode-trial') CORE = Path('/home/vpci/source') @@ -161,12 +163,21 @@ def validate_build(core, base): 'native_build_provenance') library = file_identity(base / 'llama-build/libvolparossa_llama_cpu.so', 128 * 1024 ** 2) require(value.get('library') == {'path': 'libvolparossa_llama_cpu.so', **library}, 'native_library_binding') - names = SESSION_WRAPPERS if session else (CONSTRAINED_WRAPPERS if constrained else LEGACY_WRAPPERS) + # The exact core parser above validates this atomic pair. Build v4 alone + # cannot distinguish a historical seven-source build from the correction. + aligned = 'cpu_source_overlay' in value + require(not any(key in value for key in CPU_FIELDS) or + (session and aligned and type(value['cpu_source_overlay']) is dict + and value.get('cpu_compile_source_verified') is True), 'native_build_cpu') + names = ALIGNED_WRAPPERS if aligned else ( + SESSION_WRAPPERS if session else (CONSTRAINED_WRAPPERS if constrained else LEGACY_WRAPPERS)) wrapper = {name: file_identity(core / 'workers/volparossa-ml/native-cpu' / name, 1024 ** 2) for name in names} require(value.get('wrapper_sources') == wrapper, 'native_wrapper_binding') builder.verify_loader_compilation(base / 'llama-build', base / 'llama-build/loader-overlay/llama-model-loader.cpp') + if aligned: + builder.verify_cpu_compilation(base / 'llama-build', base / 'llama-source') require(type(value.get('dynamic_dependencies')) is list and value['dynamic_dependencies'] and len(set(value['dynamic_dependencies'])) == len(value['dynamic_dependencies']) and set(value['dynamic_dependencies']) <= builder.ALLOWED_NEEDED, 'native_dependency_binding') @@ -177,6 +188,8 @@ def validate_build(core, base): result.update(build_version=value['version'], output_constraints=[OUTPUT_CONSTRAINT]) if session: result['native_sessions'] = [NATIVE_SESSION] + if aligned: + result.update({key: value[key] for key in CPU_FIELDS}) return result @@ -215,6 +228,9 @@ def validate_provision(core, base, report, build): else 'output_constraints' not in copied_build), 'native_copied_build_constraint') require(copied_build.get('native_sessions') == [NATIVE_SESSION] if session else 'native_sessions' not in copied_build, 'native_copied_build_session') + cpu = {key: copied_build[key] for key in CPU_FIELDS if key in copied_build} + require(cpu == {key: build[key] for key in CPU_FIELDS if key in build}, + 'native_copied_build_cpu') for name, maximum in (('library', 128 * 1024 ** 2), ('gguf', 9 * 1024 ** 3)): require(value[name] == {'path': value[name]['path'], **file_identity(root / value[name]['path'], maximum)}, 'native_artifact_binding') @@ -228,11 +244,12 @@ def validate_provision(core, base, report, build): output_constraints=[OUTPUT_CONSTRAINT]) if session: result['native_sessions'] = [NATIVE_SESSION] + result.update(cpu) return result def require_output_constraint(build, backend): - """Current trial requires both constrained output and the owner-session ABI. + """Current trial requires constrained output, owner sessions and CPU correction. Historical validators above remain readable; neither an old constrained build nor a session capability on only one artifact authorizes this trial. @@ -242,6 +259,14 @@ def require_output_constraint(build, backend): and build.get('output_constraints') == backend.get('output_constraints') == [OUTPUT_CONSTRAINT] and build.get('native_sessions') == backend.get('native_sessions') == [NATIVE_SESSION], 'native_output_constraint_required') + # These closed projections come from validate_build/validate_provision, + # which bind the exact core-owned patch and real compiler-input checks. + require(build.get('cpu_compile_source_verified') is True + and backend.get('cpu_compile_source_verified') is True + and type(build.get('cpu_source_overlay')) is dict + and build['cpu_source_overlay'].get('kind') == 'q1_avx2_aligned_load_v1' + and build['cpu_source_overlay'] == backend.get('cpu_source_overlay'), + 'native_cpu_correction_required') def guest_build(deadline=None): diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index 2b777de..c31bdad 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = '95ef4541e518f9f9050dedc5d24bd4176b70db71' +NATIVE_CORE = '6d65bb12ed7b8720d43868c9306313133a5b96ae' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 7ca5f47..46049df 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '95ef4541e518f9f9050dedc5d24bd4176b70db71') + self.assertEqual(TRIAL.NATIVE_CORE, '6d65bb12ed7b8720d43868c9306313133a5b96ae') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): @@ -691,7 +691,15 @@ def fail(native, listener, nonce, deadline, limit, observed): TRIAL.run_native_build(11 * TRIAL.GIB, {}) self.assertEqual(start.call_count, 1) - def bundle_fixture(self, directory, constrained=False, session=False): + def cpu_overlay(self): + # Inert provenance fixture; actual core hashes are checked separately. + return dict(kind='q1_avx2_aligned_load_v1', original_path='ggml/src/ggml-cpu/arch/x86/quants.c', + original_sha256='1' * 64, effective_sha256='2' * 64, + patch_path='native-cpu/q1-aligned-load.patch', patch_sha256='3' * 64, + compiled_source='cpu-overlay/quants.c') + + def bundle_fixture(self, directory, constrained=False, session=False, aligned=False): + session = session or aligned constrained = constrained or session base, core = Path(directory) / 'base', Path(directory) / 'core' build_root, backend = base / 'llama-build', base / 'ml/native-backend' @@ -703,8 +711,8 @@ def artifact(path, content): return {'bytes': len(content), 'sha256': hashlib.sha256(content).hexdigest()} library = artifact(build_root / 'libvolparossa_llama_cpu.so', b'inert library bytes, never loaded') (backend / 'libvolparossa_llama_cpu.so').write_bytes((build_root / 'libvolparossa_llama_cpu.so').read_bytes()) - names = NATIVE.SESSION_WRAPPERS if session else ( - NATIVE.CONSTRAINED_WRAPPERS if constrained else NATIVE.LEGACY_WRAPPERS) + names = NATIVE.ALIGNED_WRAPPERS if aligned else (NATIVE.SESSION_WRAPPERS if session else ( + NATIVE.CONSTRAINED_WRAPPERS if constrained else NATIVE.LEGACY_WRAPPERS)) wrapper = {name: artifact(wrappers / name, b'inert source ' + name.encode()) for name in names} raw_build = dict(version=4 if session else (3 if constrained else 2), kind=KIND, source_commit=NATIVE.SOURCE, source_tree='b' * 40, @@ -716,6 +724,8 @@ def artifact(path, content): raw_build['output_constraints'] = [NATIVE.OUTPUT_CONSTRAINT] if session: raw_build['native_sessions'] = [NATIVE.NATIVE_SESSION] + if aligned: + raw_build.update(cpu_source_overlay=self.cpu_overlay(), cpu_compile_source_verified=True) for root in (build_root, backend): (root / 'build.json').write_text(json.dumps(raw_build)) build_hash = hashlib.sha256((build_root / 'build.json').read_bytes()).hexdigest() @@ -736,7 +746,7 @@ def artifact(path, content): parser = SimpleNamespace(validate_manifest=Mock(), validate_build_provenance=Mock()) builder = SimpleNamespace(SOURCE=NATIVE.SOURCE, ORIGIN=NATIVE.ORIGIN, KIND=KIND, ALLOWED_NEEDED={'libc.so.6'}, verify_source=lambda _path: 'b' * 40, - native=parser, verify_loader_compilation=Mock()) + native=parser, verify_loader_compilation=Mock(), verify_cpu_compilation=Mock()) return base, core, report, builder, parser def test_exact_build_and_converted_manifest_bind_actual_inert_bytes(self): @@ -806,7 +816,8 @@ def test_session_build4_backend3_bind_seven_exact_sources_without_execution_clai build = NATIVE.validate_build(core, base) with patch.object(NATIVE, 'module', return_value=parser): backend = NATIVE.validate_provision(core, base, report, build) - NATIVE.require_output_constraint(build, backend) + with self.assertRaisesRegex(ValueError, 'native_cpu_correction_required'): + NATIVE.require_output_constraint(build, backend) self.assertEqual((build['build_version'], backend['build_version'], backend['backend_version']), (4, 4, 3)) self.assertEqual(build['native_sessions'], [NATIVE.NATIVE_SESSION]) @@ -851,6 +862,120 @@ def test_session_build_capability_or_source_substitution_is_rejected(self): with patch.object(NATIVE, 'module', return_value=builder), self.assertRaises((OSError, ValueError)): NATIVE.validate_build(core, base) + def test_aligned_build_binds_eight_sources_and_closed_cpu_projections(self): + with tempfile.TemporaryDirectory() as temp: + base, core, report, builder, parser = self.bundle_fixture(temp, aligned=True) + with patch.object(NATIVE, 'module', return_value=builder): + build = NATIVE.validate_build(core, base) + builder.verify_cpu_compilation.assert_called_once_with(base / 'llama-build', base / 'llama-source') + with patch.object(NATIVE, 'module', return_value=parser): + backend = NATIVE.validate_provision(core, base, report, build) + NATIVE.require_output_constraint(build, backend) + self.assertEqual(len(build['wrapper_sources']), 8) + self.assertEqual(set(build['wrapper_sources']), set(NATIVE.ALIGNED_WRAPPERS)) + self.assertEqual(set(NATIVE.ALIGNED_WRAPPERS) - set(NATIVE.SESSION_WRAPPERS), + {'q1-aligned-load.patch'}) + for result in (build, backend): + self.assertEqual(result['cpu_source_overlay'], self.cpu_overlay()) + self.assertIs(result['cpu_compile_source_verified'], True) + self.assertNotIn('passed', result) + self.assertNotIn(str(base), json.dumps(result)) + self.assertFalse(build['models_loaded']) + + def test_cpu_pair_cannot_be_partial_false_or_downgraded(self): + mutations = [{'cpu_compile_source_verified': flag} for flag in (False, 1, None, 'true')] + mutations += [{'cpu_source_overlay': None}, {'version': 3}, {'version': 2}] + for changes in mutations + ['cpu_source_overlay', 'cpu_compile_source_verified', 'both']: + with self.subTest(changes=changes), tempfile.TemporaryDirectory() as temp: + base, core, _, builder, _ = self.bundle_fixture(temp, aligned=True) + path = base / 'llama-build/build.json' + value = json.loads(path.read_text()) + if type(changes) is dict: + value.update(changes) + else: + for key in NATIVE.CPU_FIELDS if changes == 'both' else (changes,): + del value[key] + path.write_text(json.dumps(value)) + with patch.object(NATIVE, 'module', return_value=builder), self.assertRaises(ValueError): + NATIVE.validate_build(core, base) + builder.verify_cpu_compilation.assert_not_called() + + def test_cpu_patch_inventory_and_content_cannot_be_substituted(self): + for mutation in ('missing', 'modified', 'unlisted', 'extra'): + with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as temp: + base, core, _, builder, _ = self.bundle_fixture(temp, aligned=True) + patch_path = core / 'workers/volparossa-ml/native-cpu/q1-aligned-load.patch' + if mutation == 'missing': + patch_path.unlink() + elif mutation == 'modified': + patch_path.write_bytes(b'changed, never executed') + else: + path = base / 'llama-build/build.json' + value = json.loads(path.read_text()) + if mutation == 'unlisted': + del value['wrapper_sources']['q1-aligned-load.patch'] + else: + value['wrapper_sources']['extra.patch'] = value['wrapper_sources']['q1-aligned-load.patch'] + path.write_text(json.dumps(value)) + with patch.object(NATIVE, 'module', return_value=builder), self.assertRaises((OSError, ValueError)): + NATIVE.validate_build(core, base) + builder.verify_cpu_compilation.assert_not_called() + + def test_cpu_core_and_compilation_rejections_are_not_swallowed(self): + with tempfile.TemporaryDirectory() as temp: + base, core, _, builder, parser = self.bundle_fixture(temp, aligned=True) + with patch.object(NATIVE, 'module', return_value=builder): + parser.validate_build_provenance.side_effect = ValueError('CORE_CPU_REJECTED') + with self.assertRaisesRegex(ValueError, 'CORE_CPU_REJECTED'): + NATIVE.validate_build(core, base) + builder.verify_cpu_compilation.assert_not_called() + parser.validate_build_provenance.side_effect = None + builder.verify_cpu_compilation.side_effect = ValueError('CORE_CPU_COMPILE_REJECTED') + with self.assertRaisesRegex(ValueError, 'CORE_CPU_COMPILE_REJECTED'): + NATIVE.validate_build(core, base) + builder.verify_cpu_compilation.assert_called_once() + + def test_copied_cpu_provenance_must_match_the_verified_build(self): + for mutation in ('missing', 'changed', 'false', 'copied_bytes'): + with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as temp: + base, core, report, builder, parser = self.bundle_fixture(temp, aligned=True) + with patch.object(NATIVE, 'module', return_value=builder): + build = NATIVE.validate_build(core, base) + if mutation == 'missing': + del build['cpu_source_overlay'] + elif mutation == 'changed': + build['cpu_source_overlay']['effective_sha256'] = 'f' * 64 + elif mutation == 'false': + build['cpu_compile_source_verified'] = False + else: + path = base / 'ml/native-backend/build.json' + path.write_bytes(path.read_bytes() + b' ') + with patch.object(NATIVE, 'module', return_value=parser), \ + self.assertRaisesRegex(ValueError, 'native_copied_build_(cpu|binding)'): + NATIVE.validate_provision(core, base, report, build) + + def test_trial_requires_matching_cpu_correction_on_both_projections(self): + build = dict(build_version=4, output_constraints=[NATIVE.OUTPUT_CONSTRAINT], + native_sessions=[NATIVE.NATIVE_SESSION], cpu_source_overlay=self.cpu_overlay(), + cpu_compile_source_verified=True) + backend = dict(build, backend_version=3) + for side in ('build', 'backend'): + for field, values in (('cpu_source_overlay', (None, {}, 'unknown', dict(self.cpu_overlay(), + effective_sha256='f' * 64))), + ('cpu_compile_source_verified', (None, False, 1, 'true'))): + for value in values: + a, b = copy.deepcopy(build), copy.deepcopy(backend) + (a if side == 'build' else b)[field] = value + with self.subTest(side=side, field=field, value=value), \ + self.assertRaisesRegex(ValueError, 'native_cpu_correction_required'): + NATIVE.require_output_constraint(a, b) + for a, b in (({key: value for key, value in build.items() if key not in NATIVE.CPU_FIELDS}, + {key: value for key, value in backend.items() if key not in NATIVE.CPU_FIELDS}), + (dict(build, cpu_source_overlay={'kind': 'unreviewed'}), + dict(backend, cpu_source_overlay={'kind': 'unreviewed'}))): + with self.assertRaisesRegex(ValueError, 'native_cpu_correction_required'): + NATIVE.require_output_constraint(a, b) + def test_session_backend_rejects_missing_mixed_and_copied_capability(self): for changes in ({'version': True}, {'version': 4}, {'version': 2}, {'native_sessions': None}, {'native_sessions': []}, @@ -905,7 +1030,8 @@ def test_legacy_versions_reject_unsolicited_session_capability(self): def test_current_trial_requires_exact_session_on_both_closed_projections(self): build = dict(build_version=4, output_constraints=[NATIVE.OUTPUT_CONSTRAINT], - native_sessions=[NATIVE.NATIVE_SESSION]) + native_sessions=[NATIVE.NATIVE_SESSION], cpu_source_overlay=self.cpu_overlay(), + cpu_compile_source_verified=True) backend = dict(build, backend_version=3) NATIVE.require_output_constraint(build, backend) for field in ('build_version', 'output_constraints', 'native_sessions'): @@ -1107,6 +1233,55 @@ def test_actual_pinned_core_compile_source_proof_rejects_tampering(self): builder.verify_loader_compilation(output, loader) library.assert_not_called() + @unittest.skipUnless(os.environ.get('VOLPAROSSA_NATIVE_CORE_TEST_SOURCE'), + 'requires explicitly retained exact core source; never builds') + def test_actual_pinned_core_cpu_pair_and_compile_input_checks(self): + with patch('ctypes.CDLL') as library: + builder = self.actual_core_builder() + native = builder.native + value = dict(version=4, source_commit=native.SOURCE, source_tree=native.SOURCE_TREE, + source_overlay=native.loader_provenance(), loader_compile_source_verified=True, + upstream_original_unchanged=True, output_constraints=[native.OUTPUT_CONSTRAINT], + native_sessions=[native.NATIVE_SESSION], cpu_source_overlay=native.cpu_provenance(), + cpu_compile_source_verified=True) + native.validate_build_provenance(value, NATIVE.require) + for mutation in ('cpu_source_overlay', 'cpu_compile_source_verified', 'version', 'hash', 'extra'): + changed = copy.deepcopy(value) + if mutation in NATIVE.CPU_FIELDS: + del changed[mutation] + elif mutation == 'version': + changed['version'] = 3 + del changed['native_sessions'] + elif mutation == 'hash': + changed['cpu_source_overlay']['effective_sha256'] = '0' * 64 + else: + changed['cpu_source_overlay']['extra'] = True + with self.subTest(mutation=mutation), self.assertRaisesRegex(ValueError, 'NATIVE_CPU_PATCH_BINDING'): + native.validate_build_provenance(changed, NATIVE.require) + # Exercise the exact core verifier on inert compiler metadata. + # This is not a compiler execution or a real native-source receipt. + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + output, source = root / 'output', root / 'upstream' + staged = output / 'cpu-overlay/quants.c' + staged.parent.mkdir(parents=True) + raw = b'inert CPU fixture; never compiled' + staged.write_bytes(raw) + database = output / 'build/compile_commands.json' + database.parent.mkdir() + row = dict(directory=str(database.parent), file=str(staged), + command='cc -o CMakeFiles/ggml-cpu.dir/fixture.o -c ' + str(staged)) + with patch.object(native, 'CPU_EFFECTIVE_SHA', hashlib.sha256(raw).hexdigest()): + database.write_text(json.dumps([row])) + builder.verify_cpu_compilation(output, source) + for changed in (dict(row, file=str(source / native.CPU_ORIGINAL_PATH)), + dict(row, command='cc -o CMakeFiles/wrong.dir/x.o -c ' + str(staged)), + dict(row, command='cc -o CMakeFiles/ggml-cpu.dir/x.o -c wrong.c')): + database.write_text(json.dumps([changed])) + with self.subTest(changed=changed), self.assertRaisesRegex(ValueError, 'NATIVE_COMPILED_CPU'): + builder.verify_cpu_compilation(output, source) + library.assert_not_called() + if __name__ == '__main__': unittest.main() From 370348e7fae003aadf0e134c9b881a079189d1f6 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:53:23 +0200 Subject: [PATCH 35/39] fix: bind OpenCode edit approvals to the workspace target --- src/opencode-task.cjs | 25 ++++- tests/opencode-edit-target.test.cjs | 165 ++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 tests/opencode-edit-target.test.cjs diff --git a/src/opencode-task.cjs b/src/opencode-task.cjs index 78cce5e..752e96e 100644 --- a/src/opencode-task.cjs +++ b/src/opencode-task.cjs @@ -48,6 +48,23 @@ class OpenCodeTask { const resolved = path.posix.resolve(this.client.workspace, value); return resolved === this.client.workspace || resolved.startsWith(this.client.workspace + '/'); } + editPatterns(input, request) { + // Pinned OpenCode uses worktree-relative patterns: non-Git worktree '/' is + // not the selected workspace. Bind the running edit input and its metadata + // first; normalize only this exact target for the owner's unchanged policy. + // This is lexical scope, not a filesystem/symlink or execution-authority proof. + const clean = value => bounded(value, 4096) && value.length > 0 && + !/[\\?*\[\]{}\x00-\x1f\x7f]/.test(value) && !value.split('/').includes('..'); + const target = request.metadata.filepath; + if (!clean(input.filePath) || !clean(target) || !path.posix.isAbsolute(target) || + path.posix.resolve(target) !== target || !this.within(target) || + path.posix.resolve(this.client.workspace, input.filePath) !== target || + request.patterns.length !== 1 || !clean(request.patterns[0])) return null; + const relative = path.posix.relative(this.client.workspace, target); + const spellings = [relative, `./${relative}`, target, path.posix.relative('/', target)]; + if (!relative || !spellings.includes(request.patterns[0])) return null; + return [relative]; + } toolKey(session, message, call) { return `${session}/${message}/${call}`; } get diagnostics() { return JSON.parse(JSON.stringify(this.taskDiagnostic)); } observeTool(key, part) { @@ -82,18 +99,20 @@ class OpenCodeTask { const tool = this.tools.get(this.toolKey(request.sessionID, request.tool.messageID, request.tool.callID)); if (tool && tool.state.status === 'running' && record(tool.state.input)) { const input = tool.state.input; - const command = request.permission === 'bash' ? input.command : `Edit ${request.patterns.join(', ')}`; + const singleEdit = request.permission === 'edit' && tool.tool === 'edit'; + const patterns = singleEdit ? this.editPatterns(input, request) : request.patterns; + const command = request.permission === 'bash' ? input.command : `Edit ${(patterns ?? request.patterns).join(', ')}`; const directory = input.workdir ?? this.client.workspace; const eligible = request.permission === 'bash' ? tool.tool === 'bash' && bounded(command, 8192) && command.trim() && this.within(directory) - : ['edit', 'write', 'apply_patch', 'multiedit'].includes(tool.tool) && + : singleEdit ? patterns !== null : ['write', 'apply_patch', 'multiedit'].includes(tool.tool) && request.patterns.every(value => this.within(value) && !/[?*\[\]{}]/.test(value)) && (request.metadata.filepath === undefined || this.within(request.metadata.filepath)); if (eligible && !this.stopped) { this.taskDiagnostic.permissions.forwarded++; let timer; const decision = Promise.resolve().then(() => this.approval({ - permission: request.permission, patterns: [...request.patterns], metadata: request.metadata, + permission: request.permission, patterns: [...patterns], metadata: request.metadata, command, directory, sessionID: request.sessionID, child: request.sessionID !== this.session, })).then(value => value === true, () => false); try { diff --git a/tests/opencode-edit-target.test.cjs b/tests/opencode-edit-target.test.cjs new file mode 100644 index 0000000..b8f0c73 --- /dev/null +++ b/tests/opencode-edit-target.test.cjs @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-3.0-only +'use strict'; +const test = require('node:test'); +const assert = require('node:assert/strict'); +const {EventEmitter} = require('node:events'); +const {OpenCodeTask, MODEL} = require('../src/opencode-task.cjs'); +const {approvalKind} = require('../scripts/smoke_opencode_inference.cjs'); + +class Client extends EventEmitter { + constructor(events) { super(); this.workspace = '/workspace'; this.ready = true; this.events = events; this.calls = []; } + async createSession() { return {id: 'ses_root', directory: this.workspace, model: {id: MODEL, providerID: 'volparossa'}}; } + async getSession() { return undefined; } + async prompt() { + for (const event of this.events) this.emit('event', event); + return {info: {id: 'msg_result', sessionID: 'ses_root', role: 'assistant', providerID: 'volparossa', modelID: MODEL, + finish: 'stop', time: {completed: 1}, path: {cwd: this.workspace}}, parts: []}; + } + async replyPermission(id, accepted) { this.calls.push(['permission', id, accepted]); return true; } + async abort(id) { this.calls.push(['abort', id]); return true; } + async deleteSession(id) { this.calls.push(['delete', id]); return true; } +} + +function events({input = {filePath: '/workspace/fixture.py'}, patterns = ['workspace/fixture.py'], + metadata = {filepath: '/workspace/fixture.py', diff: 'synthetic local diff'}, kind = 'edit', request = {}} = {}) { + return [{type: 'message.part.updated', properties: {sessionID: 'ses_root', part: { + id: 'prt_tool', type: 'tool', sessionID: 'ses_root', messageID: 'msg_tool', callID: 'call_edit', tool: kind, + state: {status: 'running', input}}}}, + {type: 'permission.asked', properties: {id: 'per_edit', sessionID: 'ses_root', permission: 'edit', + patterns, metadata, always: ['*'], tool: {messageID: 'msg_tool', callID: 'call_edit'}, ...request}}]; +} + +async function exercise(options, approve = proposal => approvalKind(proposal) === 'edit') { + const original = events(options), before = structuredClone(original); + const client = new Client(original), proposals = []; + const task = new OpenCodeTask(client, async proposal => { proposals.push(proposal); return approve(proposal); }); + await task.run('Synthetic approval contract, not a model execution.'); + assert.deepEqual(original, before, 'normalization must not rewrite upstream events'); + assert.deepEqual(client.calls.at(-1), ['delete', 'ses_root']); + return {client, task, proposals}; +} + +test('non-git upstream edit reaches unchanged exact fixture approval through real task events', async () => { + // Pinned aec0 project.resolve uses worktree '/' without Git. EditTool asks for + // path.relative('/', '/workspace/fixture.py'), not workspace-relative spelling. + const {client, task, proposals} = await exercise(); + assert.deepEqual(client.calls.filter(call => call[0] === 'permission'), [['permission', 'per_edit', true]]); + assert.deepEqual(proposals[0].patterns, ['fixture.py']); + assert.equal(proposals[0].metadata.filepath, '/workspace/fixture.py'); + assert.equal(approvalKind(proposals[0]), 'edit'); + assert.deepEqual(task.diagnostics.permissions, {requested: 1, forwarded: 1, accepted: 1, rejected: 0, unconfirmed: 0}); +}); + +for (const pattern of ['fixture.py', './fixture.py', '/workspace/fixture.py', 'workspace/fixture.py']) { + test(`single edit binds supported target spelling ${pattern}`, async () => { + for (const filePath of ['/workspace/fixture.py', 'fixture.py', './fixture.py']) { + const {client, proposals} = await exercise({input: {filePath}, patterns: [pattern]}); + assert.deepEqual(proposals[0].patterns, ['fixture.py']); + assert.equal(proposals[0].command, 'Edit fixture.py'); + assert.deepEqual(client.calls[0], ['permission', 'per_edit', true]); + } + }); +} + +const rejected = { + 'missing input target': {input: {}}, + 'non-string input target': {input: {filePath: true}}, + 'different input target': {input: {filePath: '/workspace/other.py'}}, + 'outside input target': {input: {filePath: '/workspace-other/fixture.py'}}, + 'input parent traversal': {input: {filePath: '../workspace/fixture.py'}}, + 'input internal traversal': {input: {filePath: 'sub/../fixture.py'}}, + 'input backslash': {input: {filePath: 'sub\\fixture.py'}}, + 'input wildcard': {input: {filePath: 'fixture.*'}}, + 'missing metadata target': {metadata: {}}, + 'non-string metadata target': {metadata: {filepath: null}}, + 'different metadata target': {metadata: {filepath: '/workspace/other.py'}}, + 'relative metadata target': {metadata: {filepath: 'fixture.py'}}, + 'noncanonical metadata target': {metadata: {filepath: '/workspace/./fixture.py'}}, + 'metadata double separator': {metadata: {filepath: '/workspace//fixture.py'}}, + 'metadata trailing separator': {input: {filePath: '/workspace/fixture.py/'}, + metadata: {filepath: '/workspace/fixture.py/'}, patterns: ['fixture.py']}, + 'metadata traversal': {metadata: {filepath: '/workspace/sub/../fixture.py'}}, + 'metadata control byte': {metadata: {filepath: '/workspace/fixture.py\n'}}, + 'outside matched target': {input: {filePath: '/tmp/fixture.py'}, + metadata: {filepath: '/tmp/fixture.py'}, patterns: ['tmp/fixture.py']}, + 'workspace prefix collision': {input: {filePath: '/workspace-other/fixture.py'}, + metadata: {filepath: '/workspace-other/fixture.py'}, patterns: ['workspace-other/fixture.py']}, + 'workspace itself': {input: {filePath: '/workspace'}, metadata: {filepath: '/workspace'}, patterns: ['workspace']}, + 'missing pattern': {patterns: []}, + 'multiple patterns': {patterns: ['fixture.py', 'test_fixture.py']}, + 'wrong pattern target': {patterns: ['workspace/other.py']}, + 'pattern wildcard': {patterns: ['workspace/*.py']}, + 'pattern traversal': {patterns: ['workspace/sub/../fixture.py']}, + 'pattern outside traversal': {patterns: ['../workspace/fixture.py']}, + 'pattern double separator': {patterns: ['workspace//fixture.py']}, + 'pattern control byte': {patterns: ['workspace/fixture.py\0']}, + 'overlong pattern': {patterns: ['x'.repeat(4097)]}, + 'foreign session': {request: {sessionID: 'ses_other'}}, + 'foreign message': {request: {tool: {messageID: 'msg_other', callID: 'call_edit'}}}, + 'foreign call': {request: {tool: {messageID: 'msg_tool', callID: 'call_other'}}}, + 'different permission': {request: {permission: 'external_directory'}}, +}; +for (const [name, options] of Object.entries(rejected)) { + test(`single edit rejects ${name} before owner approval`, async () => { + const {client, task, proposals} = await exercise(options, () => assert.fail('unbound target reached owner')); + assert.equal(proposals.length, 0); + assert.deepEqual(client.calls[0], ['permission', 'per_edit', false]); + assert.deepEqual(task.diagnostics.permissions, {requested: 1, forwarded: 0, accepted: 0, rejected: 1, unconfirmed: 0}); + }); +} + +test('normalized targets never broaden the exact fixture policy or use a basename shortcut', async () => { + for (const target of ['test_fixture.py', 'other.py', 'sub/fixture.py']) { + const filepath = '/workspace/' + target; + const {client, proposals} = await exercise({input: {filePath: filepath}, + metadata: {filepath}, patterns: ['workspace/' + target]}); + assert.deepEqual(proposals[0].patterns, [target]); + assert.equal(approvalKind(proposals[0]), null); + assert.deepEqual(client.calls[0], ['permission', 'per_edit', false]); + } +}); + +test('non-edit tool permission payloads are not normalized', async () => { + for (const kind of ['write', 'apply_patch', 'multiedit']) { + const {client, proposals} = await exercise({kind}); + assert.deepEqual(proposals[0].patterns, ['workspace/fixture.py']); + assert.equal(proposals[0].command, 'Edit workspace/fixture.py'); + assert.deepEqual(client.calls[0], ['permission', 'per_edit', false]); + } + const {client, proposals} = await exercise({kind: 'bash', input: {command: 'python3 -m unittest'}, + patterns: ['python3 -m unittest'], metadata: {}, request: {permission: 'bash'}}, proposal => approvalKind(proposal) === 'test'); + assert.deepEqual(proposals[0].patterns, ['python3 -m unittest']); + assert.deepEqual(client.calls[0], ['permission', 'per_edit', true]); +}); + +test('normalized edit is still one-shot and cannot replay its permission', async () => { + const original = events(); original.push(structuredClone(original[1])); + const client = new Client(original); let approvals = 0; + const task = new OpenCodeTask(client, async proposal => { approvals++; return approvalKind(proposal) === 'edit'; }); + await assert.rejects(task.run('Synthetic replay'), /permission_replay/); + assert.equal(approvals, 1); + assert.deepEqual(client.calls.filter(call => call[0] === 'permission'), [['permission', 'per_edit', true]]); + assert.deepEqual(client.calls.at(-1), ['delete', 'ses_root']); +}); + +test('cancel during normalized edit approval rejects late permission and joins task cleanup', async () => { + const controller = new AbortController(), client = new Client(events()); let release; + const task = new OpenCodeTask(client, proposal => { + assert.equal(approvalKind(proposal), 'edit'); + queueMicrotask(() => controller.abort()); + return new Promise(resolve => { release = resolve; }); + }); + await assert.rejects(task.run('Synthetic cancellation', {signal: controller.signal}), /cancelled/); + release(true); + assert.deepEqual(client.calls.filter(call => call[0] === 'permission'), [['permission', 'per_edit', false]]); + assert.ok(client.calls.some(call => call[0] === 'abort')); + assert.deepEqual(client.calls.at(-1), ['delete', 'ses_root']); +}); + +test('normalized edit still requires an affirmative owner decision before its approval deadline', async () => { + const client = new Client(events()); + const task = new OpenCodeTask(client, () => new Promise(() => {}), {approvalMs: 1}); + await task.run('Synthetic bounded owner approval'); + assert.deepEqual(client.calls.filter(call => call[0] === 'permission'), [['permission', 'per_edit', false]]); + assert.deepEqual(client.calls.at(-1), ['delete', 'ses_root']); +}); From ab64003003b2c42e420e95acfab11cb9fc7a4e8d Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:12:42 +0200 Subject: [PATCH 36/39] fix: observe native task memory before its cgroup exits --- scripts/opencode_task_holder.py | 244 +++++++++++++ scripts/smoke_opencode_inference.py | 119 ++++++- tests/test_opencode_ci.py | 10 +- tests/test_opencode_task_holder.py | 507 ++++++++++++++++++++++++++++ 4 files changed, 860 insertions(+), 20 deletions(-) create mode 100644 scripts/opencode_task_holder.py create mode 100644 tests/test_opencode_task_holder.py diff --git a/scripts/opencode_task_holder.py b/scripts/opencode_task_holder.py new file mode 100644 index 0000000..f015ed5 --- /dev/null +++ b/scripts/opencode_task_holder.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-3.0-only +"""Fixed guest task lifetime: observe the original cgroup before its last exit. + +The Node task, its permissions, private cleanup and result are unchanged. This +helper joins that child, then stays alone in the original bounded task cgroup +until its owner has read the real counters. Readiness is not task acceptance. +""" +import argparse +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import re +import signal +import socket +import stat +import struct +import subprocess +import sys +import time + +CONTROL = 'task-observation.sock' +FRAME_MAX = 1024 +TASK_SECONDS = 2550 +OWNER_SECONDS = 2565 + + +def require(value, reason='task_holder_contract'): + if not value: + raise ValueError(reason) + + +def remaining(deadline): + require(type(deadline) is int and 0 < deadline < 2 ** 63) + seconds = (deadline - time.monotonic_ns()) / 1_000_000_000 + require(0 < seconds <= OWNER_SECONDS, 'task_holder_deadline') + return seconds + + +def binding(kind, nonce, status, receipt): + require(kind in ('ready', 'release') and type(nonce) is str + and re.fullmatch('[0-9a-f]{64}', nonce)) + require(type(status) is int and -64 <= status <= 255 + and (receipt is None or type(receipt) is str and re.fullmatch('[0-9a-f]{64}', receipt))) + return (json.dumps(dict(version=1, kind='task_' + kind, nonce=nonce, + child_returncode=status, receipt_sha256=receipt), + sort_keys=True, separators=(',', ':')) + '\n').encode() + + +def decode_ready(raw, nonce): + # Re-encoding also rejects duplicate/extra keys, bools, whitespace and EOF + # ambiguity. The peer is a fixed source-bound helper, never a remote client. + value = json.loads(raw) + require(type(value) is dict) + expected = binding('ready', nonce, value.get('child_returncode'), value.get('receipt_sha256')) + require(raw == expected, 'task_holder_binding') + return value + + +def send_frame(channel, raw, deadline): + require(type(raw) is bytes and 0 < len(raw) <= FRAME_MAX) + channel.settimeout(remaining(deadline)) + channel.sendall(raw) + channel.shutdown(socket.SHUT_WR) + + +def receive_frame(channel, deadline): + raw = bytearray() + while True: + channel.settimeout(remaining(deadline)) + block = channel.recv(FRAME_MAX + 1 - len(raw)) + if not block: + require(0 < len(raw) <= FRAME_MAX, 'task_holder_frame') + return bytes(raw) + raw.extend(block) + require(len(raw) <= FRAME_MAX, 'task_holder_frame') + + +def peer(channel, pid): + require(type(pid) is int and 1 < pid < 2 ** 31) + observed = struct.unpack('3i', channel.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)) + require(observed == (pid, os.getuid(), os.getgid()), 'task_holder_peer') + + +def identity(info): + return info.st_dev, info.st_ino + + +def file_identity(info): + return (info.st_dev, info.st_ino, info.st_mode, info.st_uid, info.st_gid, + info.st_nlink, info.st_size, info.st_mtime_ns, info.st_ctime_ns) + + +def receipt_bytes(path): + require(path.is_absolute() and path.resolve(strict=True) == path, 'task_holder_receipt') + with os.fdopen(os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC), 'rb') as stream: + before = os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_uid == os.getuid() + and stat.S_IMODE(before.st_mode) == 0o600 and before.st_nlink == 1 + and 0 < before.st_size <= 32768, 'task_holder_receipt') + raw = stream.read(32769) + after = os.fstat(stream.fileno()) + require(len(raw) == before.st_size and file_identity(before) == file_identity(after) + and identity(path.lstat()) == identity(before), 'task_holder_receipt_changed') + return raw + + +class Group: + """One fixed task directory, held across both live-member and counter reads.""" + def __init__(self, path, pid): + require(path.is_absolute() and path.resolve(strict=True) == path, 'task_holder_cgroup') + self.path, self.pid = path, pid + self.parent = identity(path.parent.lstat()) + self.fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC) + self.bound = identity(os.fstat(self.fd)) + + def close(self): + os.close(self.fd) + + def check(self): + require(identity(self.path.parent.lstat()) == self.parent + and identity(self.path.lstat()) == self.bound + and stat.S_ISDIR(self.path.lstat().st_mode) + and identity(os.fstat(self.fd)) == self.bound, 'task_holder_cgroup_changed') + + def read(self, name): + require(name in ('memory.events', 'memory.max', 'memory.swap.max', 'memory.peak')) + self.check() + with os.fdopen(os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC, + dir_fd=self.fd), 'rb') as stream: + require(stat.S_ISREG(os.fstat(stream.fileno()).st_mode), 'task_holder_counter') + raw = stream.read(4097) + require(0 < len(raw) <= 4096, 'task_holder_counter') + self.check() + return raw + + def alone(self): + self.check() + paths = [] + for path in self.path.rglob('cgroup.procs'): + require(len(paths) < 16 and path.resolve(strict=True) == path, 'task_holder_members') + paths.append(path) + require(self.path / 'cgroup.procs' in paths, 'task_holder_members') + members = [] + for path in paths: + with os.fdopen(os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC), 'rb') as stream: + require(stat.S_ISREG(os.fstat(stream.fileno()).st_mode), 'task_holder_members') + raw = stream.read(4097) + require(len(raw) <= 4096 and all(re.fullmatch(rb'[0-9]{1,10}', row) for row in raw.splitlines()), + 'task_holder_members') + members.extend(int(row) for row in raw.splitlines()) + require(members == [self.pid], 'task_holder_members') + self.check() + + def memory(self): + self.alone() + rows = [line.split() for line in self.read('memory.events').splitlines()] + require(rows and all(len(row) == 2 and re.fullmatch(rb'[0-9]{1,20}', row[1]) for row in rows), + 'task_holder_counter') + events = dict(rows) + require(len(events) == len(rows) and b'oom_kill' in events, 'task_holder_counter') + value = {} + for key, name in (('maximum', 'memory.max'), ('swap', 'memory.swap.max'), ('peak', 'memory.peak')): + raw = self.read(name) + require(re.fullmatch(rb'[0-9]{1,20}\n?', raw), 'task_holder_counter') + value[key] = int(raw) + value['oom_kill'] = int(events[b'oom_kill']) + self.alone() + return value + + +def hold(command, path, receipt, nonce, deadline, parent_pid): + """Internal seam for inert child tests; the executable CLI never accepts argv.""" + remaining(deadline) + binding('ready', nonce, 0, None) + child = subprocess.Popen(command) + status = None + try: + status = child.wait(timeout=remaining(deadline)) + try: + digest = hashlib.sha256(receipt_bytes(receipt)).hexdigest() + except (OSError, ValueError, TypeError): + # Preserve the joined child's original status even when receipt + # ownership, metadata or readability fails. Null is no task proof. + digest = None + info = path.lstat() + require(stat.S_ISSOCK(info.st_mode) and info.st_uid == os.getuid() + and stat.S_IMODE(info.st_mode) == 0o600, 'task_holder_socket') + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as channel: + channel.settimeout(remaining(deadline)) + channel.connect(str(path)) + peer(channel, parent_pid) + send_frame(channel, binding('ready', nonce, status, digest), deadline) + require(receive_frame(channel, deadline) == binding('release', nonce, status, digest), + 'task_holder_release') + return status if status != 0 or digest is not None else 1 + except (OSError, ValueError, TypeError, subprocess.SubprocessError): + # An observation failure cannot turn a failed task into a success or + # overwrite its actual nonzero/signal status. A successful task remains + # non-accepted when the bounded release protocol fails. + return status if status not in (None, 0) else 1 + finally: + if child.poll() is None: + child.terminate() + try: + child.wait(timeout=2) + except subprocess.TimeoutExpired: + child.kill() + child.wait(timeout=2) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--deadline-monotonic-ns', type=int, required=True) + parser.add_argument('--handshake-nonce', required=True) + parser.add_argument('--parent-pid', type=int, required=True) + parser.add_argument('--inference-backend', choices=('llama_cpp_bf16_v1',), required=True) + args = parser.parse_args() + spec = importlib.util.spec_from_file_location('task_holder_trial', Path(__file__).with_name('smoke_opencode_inference.py')) + trial = importlib.util.module_from_spec(spec) + spec.loader.exec_module(trial) + require(os.getuid() > 0 and trial.pwd.getpwuid(os.getuid()).pw_name == 'vpci' + and socket.gethostname() == 'volparossa-alpha' + and trial.run(['systemd-detect-virt', '--vm'], text=True).stdout.strip() == 'kvm', + 'disposable_guest_required') + require(remaining(args.deadline_monotonic_ns) <= TASK_SECONDS) + binding('ready', args.handshake_nonce, 0, None) + status = hold(trial.task_arguments(args.inference_backend), trial.BASE / CONTROL, + trial.BASE / 'task.json', args.handshake_nonce, args.deadline_monotonic_ns, args.parent_pid) + if status < 0: + # Preserve systemd Result=signal/ExecMainStatus for this own process only. + if -status not in (signal.SIGKILL, signal.SIGSTOP): + signal.signal(-status, signal.SIG_DFL) + os.kill(os.getpid(), -status) + return status + + +if __name__ == '__main__': + try: + sys.exit(main()) + except (OSError, ValueError, TypeError, subprocess.SubprocessError): + sys.exit(1) diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index c31bdad..f943806 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -298,7 +298,7 @@ def pack(args): files = {f'code/{path.relative_to(ROOT)}': path for path in sorted((ROOT / 'src').glob('*.cjs'))} names = ['src/opencode-cooperative-tool.js', 'scripts/opencode_session.py', 'scripts/opencode_session.cjs', 'scripts/smoke_opencode_inference.cjs', 'scripts/smoke_opencode_inference.py', - 'scripts/opencode_native_cpu.py', + 'scripts/opencode_native_cpu.py', 'scripts/opencode_task_holder.py', 'third_party/opencode.json', 'third_party/opencode-LICENSE.txt', 'third_party/opencode-build-tools.json', 'patches/opencode-no-runtime-installs.patch', 'LICENSE', 'THIRD_PARTY_LICENSES.md'] @@ -535,6 +535,82 @@ def start(name, command, limit, seconds, public_source_build=False): '--setenv=RUST_LOG=off,volparossa::compute::private_diagnostic=debug', '--setenv=NO_COLOR=1', *command]) +def run_native_task(profile, backend, report): + holder = module(INPUT / 'code/scripts/opencode_task_holder.py', 'opencode_task_holder_inputs') + nonce, started = os.urandom(32).hex(), time.monotonic_ns() + deadline = started + 2550 * 1_000_000_000 + owner_deadline = started + 2565 * 1_000_000_000 + path = BASE / holder.CONTROL + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + listener.bind(str(path)) + path.chmod(0o600) + listener.listen(1) + start(TASK_UNIT, [sys.executable, '-B', str(INPUT / 'code/scripts/opencode_task_holder.py'), + '--deadline-monotonic-ns', str(deadline), '--handshake-nonce', nonce, + '--parent-pid', str(os.getpid()), '--inference-backend', backend], 768 * 1024**2, 2550) + while True: + listener.settimeout(min(2.0, holder.remaining(deadline))) + try: + channel, _ = listener.accept() + break + except TimeoutError: + state = properties(TASK_UNIT) + require(state.get('ActiveState') == 'active' and state.get('SubState') == 'running', + 'task_holder_ended_before_ready') + with channel: + state = properties(TASK_UNIT) + require(state.get('ActiveState') == 'active' and state.get('SubState') == 'running' + and re.fullmatch('[0-9]{1,10}', state.get('MainPID', '')), 'task_holder_not_running') + pid = int(state['MainPID']) + holder.peer(channel, pid) + ready = holder.decode_ready(holder.receive_frame(channel, deadline), nonce) + status = ready['child_returncode'] + # Preserve the actual Node status even if later observation fails; + # the holder's systemd terminal status is checked separately below. + report['task_exit_status'] = abs(status) + primary = None + try: + if ready['receipt_sha256'] is not None: + raw = holder.receipt_bytes(BASE / 'task.json') + require(hashlib.sha256(raw).hexdigest() == ready['receipt_sha256'], 'task_receipt_changed') + report['task'] = json.loads(raw) + validate_task_selection(report['task'], profile, backend) + else: + require(status != 0, 'task_holder_missing_success_receipt') + group = holder.Group(cgroup(TASK_UNIT), pid) + try: + report['core_memory'] = memory(CORE_UNIT) + report['task_memory'] = group.memory() + require(report['task_memory']['maximum'] == 768 * 1024**2, 'task_memory_limit') + for field in ('core_memory', 'task_memory'): + require(report[field]['swap'] == report[field]['oom_kill'] == 0, 'resource_violation') + finally: + group.close() + except BaseException as error: + primary = error + raise + finally: + # Release is cleanup, never permission to accept a bad receipt. + # Its failure must not replace the original observation failure. + try: + holder.send_frame(channel, holder.binding('release', nonce, status, + ready['receipt_sha256']), deadline) + except (OSError, ValueError): + if primary is None: + raise + while True: + holder.remaining(owner_deadline) + state = properties(TASK_UNIT) + if state.get('SubState') == 'exited' or state.get('ActiveState') in ('failed', 'inactive'): + break + time.sleep(min(.1, holder.remaining(owner_deadline))) + expected_result = 'success' if status == 0 else 'signal' if status < 0 else 'exit-code' + report['task_unit_result'] = state.get('Result') if state.get('Result') in ( + 'success', 'exit-code', 'signal', 'timeout', 'oom-kill', 'resources') else 'other' + require(state.get('ExecMainStatus') == str(abs(status)) and state.get('Result') == expected_result + and empty(TASK_UNIT), 'task_holder_exit') + + def guest(args): backend = getattr(args, 'inference_backend', 'torch') profile = trial_profile(getattr(args, 'model_profile', MODEL), backend) @@ -638,22 +714,25 @@ def guest(args): require(int(state['MainPID']) > 0, 'core_process') report['phase'] = 'opencode-task' created.append(TASK_UNIT) - start(TASK_UNIT, task_arguments(backend), 768 * 1024**2, 2550) - deadline = time.monotonic() + 2565 - while time.monotonic() < deadline: - state = properties(TASK_UNIT) - if state.get('SubState') == 'exited' or state.get('ActiveState') in ('failed', 'inactive'): - break - time.sleep(2) - report['task_unit_result'] = state.get('Result') if state.get('Result') in ( - 'success', 'exit-code', 'signal', 'timeout', 'oom-kill', 'resources') else 'other' - report['task_exit_status'] = int(state.get('ExecMainStatus', '-1')) - if (BASE / 'task.json').exists(): - report['task'] = load(BASE / 'task.json', 32768) - validate_task_selection(report['task'], profile, backend) - for name, field in ((CORE_UNIT, 'core_memory'), (TASK_UNIT, 'task_memory')): - report[field] = memory(name) - require(report[field]['swap'] == report[field]['oom_kill'] == 0, 'resource_violation') + if backend != 'torch': + run_native_task(profile, backend, report) + else: + start(TASK_UNIT, task_arguments(backend), 768 * 1024**2, 2550) + deadline = time.monotonic() + 2565 + while time.monotonic() < deadline: + state = properties(TASK_UNIT) + if state.get('SubState') == 'exited' or state.get('ActiveState') in ('failed', 'inactive'): + break + time.sleep(2) + report['task_unit_result'] = state.get('Result') if state.get('Result') in ( + 'success', 'exit-code', 'signal', 'timeout', 'oom-kill', 'resources') else 'other' + report['task_exit_status'] = int(state.get('ExecMainStatus', '-1')) + if (BASE / 'task.json').exists(): + report['task'] = load(BASE / 'task.json', 32768) + validate_task_selection(report['task'], profile, backend) + for name, field in ((CORE_UNIT, 'core_memory'), (TASK_UNIT, 'task_memory')): + report[field] = memory(name) + require(report[field]['swap'] == report[field]['oom_kill'] == 0, 'resource_violation') report['core_diagnostics'] = private.service_diagnostic( BASE / (CORE_UNIT + '.log'), **({'memory_enabled': True} if backend != 'torch' else {})) require(report['task_exit_status'] == 0 and report.get('task', {}).get('passed') is True, 'task_failed') @@ -683,7 +762,11 @@ def guest(args): # The original worker monitor owns descendants; stopping the complete # cgroup joins nested native/model processes, not just their leaders. for name in reversed(created): - if cgroup(name).exists(): + # Native task counters were read while the joined-child holder was + # still the sole member. Do not reintroduce a post-exit cgroup race + # or overwrite that bound observation during unconditional cleanup. + observed_task = backend != 'torch' and name == TASK_UNIT and 'task_memory' in report + if not observed_task and cgroup(name).exists(): try: report['core_memory' if name == CORE_UNIT else 'native_build_memory' if name == NATIVE_UNIT else 'task_memory'] = ( diff --git a/tests/test_opencode_ci.py b/tests/test_opencode_ci.py index b89e9ed..460f827 100644 --- a/tests/test_opencode_ci.py +++ b/tests/test_opencode_ci.py @@ -220,10 +220,14 @@ def test_default_guest_keeps_reviewed_task_and_acceptance_with_closed_diagnostic # default. Keep the original fingerprint; do not replace the golden. native_blocks = [child for child in ast.walk(node) if isinstance(child, ast.If) and ast.unparse(child.test) == "backend != 'torch'"] - self.assertEqual(len(native_blocks), 2) + self.assertEqual(len(native_blocks), 3) for child in reversed(sorted(native_blocks, key=lambda item: item.lineno)): lines = source.splitlines(keepends=True) - del lines[child.lineno - node.lineno:child.end_lineno - node.lineno + 1] + # The task-holder branch has an explicit unchanged Torch body. + # Specialize to that body; keep the historical golden unchanged. + replacement = [line[4:] for line in lines[child.orelse[0].lineno - node.lineno: + child.end_lineno - node.lineno + 1]] if child.orelse else [] + lines[child.lineno - node.lineno:child.end_lineno - node.lineno + 1] = replacement source = ''.join(lines) source = specialize_memory_diagnostics(source) changes = ( @@ -250,6 +254,8 @@ def test_default_guest_keeps_reviewed_task_and_acceptance_with_closed_diagnostic "'core_memory' if name == CORE_UNIT else 'task_memory'"), ("(\n native_memory(profile['core_memory_bytes']) if name == NATIVE_UNIT else memory(name))", "memory(name)"), (",\n native_cpu=backend != 'torch'", ''), + (" observed_task = backend != 'torch' and name == TASK_UNIT and 'task_memory' in report\n", ''), + ('if not observed_task and cgroup(name).exists():', 'if cgroup(name).exists():'), ) for added, original in changes: self.assertIn(added, source) diff --git a/tests/test_opencode_task_holder.py b/tests/test_opencode_task_holder.py new file mode 100644 index 0000000..f52baea --- /dev/null +++ b/tests/test_opencode_task_holder.py @@ -0,0 +1,507 @@ +# SPDX-License-Identifier: GPL-3.0-only +"""Inert real-child/socket/counter fixtures, never systemd, a VM or a model.""" +import ast +from contextlib import ExitStack +import hashlib +import importlib.util +import inspect +import json +import os +from pathlib import Path +import signal +import socket +import subprocess +import sys +import tempfile +import time +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] + + +def module(name): + spec = importlib.util.spec_from_file_location(name, ROOT / 'scripts' / (name + '.py')) + value = importlib.util.module_from_spec(spec) + spec.loader.exec_module(value) + return value + + +H = module('opencode_task_holder') +T = module('smoke_opencode_inference') +NONCE = 'a' * 64 +HASH = 'b' * 64 +BACKEND = 'llama_cpp_bf16_v1' + + +def deadline(seconds=5): + return time.monotonic_ns() + int(seconds * 1e9) + + +def counters(root, pid): + root.mkdir() + for name, value in {'cgroup.procs': str(pid), 'memory.max': str(768 * 1024**2), + 'memory.swap.max': '0', 'memory.peak': '123456', + 'memory.events': 'low 0\nhigh 0\nmax 0\noom 0\noom_kill 0'}.items(): + (root / name).write_text(value + '\n') + + +# The test-only runner imports the real helper and uses only a harmless Python +# child. Production main has no command/path option and requires the fixed VM. +RUNNER = '''import importlib.util,json,pathlib,sys +s=importlib.util.spec_from_file_location('holder',sys.argv[1]); h=importlib.util.module_from_spec(s); s.loader.exec_module(h) +status=h.hold(json.loads(sys.argv[2]),pathlib.Path(sys.argv[3]),pathlib.Path(sys.argv[4]),sys.argv[5],int(sys.argv[6]),int(sys.argv[7])) +sys.exit(status) +''' + + +class Cases(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='vp-task-') + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.receipt = self.root / 'task.json' + self.receipt.write_bytes(b'{"inert":true}\n') + self.receipt.chmod(0o600) + self.processes = [] + self.addCleanup(self.join_children) + + def join_children(self): + for process in self.processes: + if process.poll() is None: + process.kill() + process.wait(timeout=3) + + def spawn(self, path, *, status=0, nonce=NONCE, until=None, command=None): + command = command or [sys.executable, '-B', '-c', f'raise SystemExit({status})'] + process = subprocess.Popen([sys.executable, '-B', '-c', RUNNER, + str(ROOT / 'scripts/opencode_task_holder.py'), json.dumps(command), str(path), + str(self.receipt), nonce, str(until or deadline()), str(os.getpid())], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + self.processes.append(process) + return process + + def listener(self): + path = self.root / H.CONTROL + server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.addCleanup(server.close) + server.bind(str(path)) + path.chmod(0o600) + server.listen(1) + server.settimeout(3) + return path, server + + def test_frame_roundtrip_and_closed_shape(self): + raw = H.binding('ready', NONCE, 0, HASH) + self.assertEqual(H.decode_ready(raw, NONCE)['receipt_sha256'], HASH) + value = json.loads(raw) + alternatives = [raw.replace(b'"version":1', b'"version":true'), raw + b' ', + raw.replace(b'"version":1', b'"version":1,"version":1'), + json.dumps(value | {'extra': 'private-canary'}).encode()] + for key, bad in [('kind', 'task_release'), ('version', 2), ('nonce', 'c' * 64), + ('child_returncode', True), ('child_returncode', 256), + ('receipt_sha256', 'private-canary')]: + alternatives.append((json.dumps(value | {key: bad}, sort_keys=True, separators=(',', ':')) + '\n').encode()) + for raw in alternatives: + with self.subTest(raw=raw), self.assertRaises((ValueError, TypeError)): + H.decode_ready(raw, NONCE) + for key in value: + with self.subTest(missing=key), self.assertRaises(ValueError): + H.decode_ready(json.dumps({k: v for k, v in value.items() if k != key}).encode(), NONCE) + + def test_frame_eof_bound_and_original_absolute_deadline(self): + for raw in (b'', b'x' * (H.FRAME_MAX + 1)): + left, right = socket.socketpair() + with left, right: + left.sendall(raw) + left.shutdown(socket.SHUT_WR) + with self.assertRaises(ValueError): + H.receive_frame(right, deadline()) + for until in (True, 0, deadline(-1), deadline(3000)): + with self.assertRaises(ValueError): + H.remaining(until) + left, right = socket.socketpair() + with left, right, self.assertRaises(TimeoutError): + H.receive_frame(right, deadline(.03)) + + def test_peer_binds_pid_uid_gid(self): + left, right = socket.socketpair() + with left, right: + H.peer(right, os.getpid()) + with self.assertRaisesRegex(ValueError, 'task_holder_peer'): + H.peer(right, os.getpid() + 100000) + + def test_joined_child_holds_original_group_until_exact_owner_release(self): + path, server = self.listener() + process = self.spawn(path) + channel, _ = server.accept() + with channel: + H.peer(channel, process.pid) + ready = H.decode_ready(H.receive_frame(channel, deadline()), NONCE) + self.assertEqual(ready['child_returncode'], 0) + self.assertEqual(ready['receipt_sha256'], hashlib.sha256(self.receipt.read_bytes()).hexdigest()) + self.assertIsNone(process.poll()) + group_path = self.root / 'group' + counters(group_path, process.pid) + group = H.Group(group_path, process.pid) + try: + self.assertEqual(group.memory(), dict(maximum=768 * 1024**2, swap=0, peak=123456, oom_kill=0)) + finally: + group.close() + self.assertIsNone(process.poll()) + H.send_frame(channel, H.binding('release', NONCE, 0, ready['receipt_sha256']), deadline()) + self.assertEqual(process.wait(timeout=3), 0) + + def test_wrong_release_and_owner_eof_fail_closed(self): + for raw in (b'', H.binding('release', 'c' * 64, 0, HASH)): + with self.subTest(raw=raw), tempfile.TemporaryDirectory(dir=self.root) as folder: + path = Path(folder) / H.CONTROL + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server: + server.bind(str(path)); path.chmod(0o600); server.listen(1); server.settimeout(3) + process = self.spawn(path) + channel, _ = server.accept() + with channel: + H.receive_frame(channel, deadline()) + channel.sendall(raw); channel.shutdown(socket.SHUT_WR) + self.assertEqual(process.wait(timeout=3), 1) + + def test_nonzero_child_status_survives_failed_release(self): + path, server = self.listener() + process = self.spawn(path, status=17) + channel, _ = server.accept() + with channel: + ready = H.decode_ready(H.receive_frame(channel, deadline()), NONCE) + self.assertEqual(ready['child_returncode'], 17) + channel.shutdown(socket.SHUT_WR) + self.assertEqual(process.wait(timeout=3), 17) + + def test_failed_child_without_receipt_still_reports_original_status(self): + self.receipt.unlink() + path, server = self.listener() + process = self.spawn(path, status=19) + channel, _ = server.accept() + with channel: + ready = H.decode_ready(H.receive_frame(channel, deadline()), NONCE) + self.assertIsNone(ready['receipt_sha256']) + H.send_frame(channel, H.binding('release', NONCE, 19, None), deadline()) + self.assertEqual(process.wait(timeout=3), 19) + + def test_signal_status_is_closed_and_propagated_only_to_holder_itself(self): + path, server = self.listener() + process = self.spawn(path, command=[sys.executable, '-B', '-c', + 'import os,signal; os.kill(os.getpid(), signal.SIGTERM)']) + channel, _ = server.accept() + with channel: + ready = H.decode_ready(H.receive_frame(channel, deadline()), NONCE) + self.assertEqual(ready['child_returncode'], -signal.SIGTERM) + H.send_frame(channel, H.binding('release', NONCE, -signal.SIGTERM, + ready['receipt_sha256']), deadline()) + # The inert runner exits normally with the negative result; production + # main instead reproduces the signal on its own process, tested below. + process.wait(timeout=3) + trial = SimpleNamespace(BASE=self.root, task_arguments=lambda _: ['fixed-node-command'], + pwd=SimpleNamespace(getpwuid=lambda _: SimpleNamespace(pw_name='vpci')), + run=lambda *a, **kw: SimpleNamespace(stdout='kvm\n')) + spec = SimpleNamespace(loader=SimpleNamespace(exec_module=lambda _: None)) + argv = ['holder', '--deadline-monotonic-ns', str(deadline()), '--handshake-nonce', NONCE, + '--parent-pid', str(os.getpid()), '--inference-backend', BACKEND] + with patch.object(sys, 'argv', argv), patch.object(H.importlib.util, 'spec_from_file_location', return_value=spec), \ + patch.object(H.importlib.util, 'module_from_spec', return_value=trial), \ + patch.object(H.os, 'getuid', return_value=985), \ + patch.object(H.socket, 'gethostname', return_value='volparossa-alpha'), \ + patch.object(H, 'hold', return_value=-signal.SIGTERM), \ + patch.object(H.signal, 'signal') as handler, patch.object(H.os, 'kill') as kill: + self.assertEqual(H.main(), -signal.SIGTERM) + handler.assert_called_once_with(signal.SIGTERM, signal.SIG_DFL) + kill.assert_called_once_with(os.getpid(), signal.SIGTERM) + + def test_success_without_receipt_retains_status_but_cannot_succeed(self): + self.receipt.unlink() + path, server = self.listener() + process = self.spawn(path) + channel, _ = server.accept() + with channel: + ready = H.decode_ready(H.receive_frame(channel, deadline()), NONCE) + self.assertEqual(ready['child_returncode'], 0) + self.assertIsNone(ready['receipt_sha256']) + H.send_frame(channel, H.binding('release', NONCE, 0, None), deadline()) + self.assertEqual(process.wait(timeout=3), 1) + + def test_original_deadline_terminates_and_joins_running_child(self): + pidfile = self.root / 'child.pid' + command = [sys.executable, '-B', '-c', + 'import os,pathlib,sys,time; pathlib.Path(sys.argv[1]).write_text(str(os.getpid())); time.sleep(60)', + str(pidfile)] + process = self.spawn(self.root / H.CONTROL, until=deadline(.3), command=command) + self.assertEqual(process.wait(timeout=3), 1) + pid = int(pidfile.read_text()) + with self.assertRaises(ProcessLookupError): + os.kill(pid, 0) + + def test_receipt_permissions_links_and_bounds_are_real(self): + self.assertEqual(H.receipt_bytes(self.receipt), b'{"inert":true}\n') + self.receipt.chmod(0o644) + with self.assertRaises(ValueError): H.receipt_bytes(self.receipt) + self.receipt.chmod(0o600) + linked = self.root / 'linked'; os.link(self.receipt, linked) + with self.assertRaises(ValueError): H.receipt_bytes(self.receipt) + linked.unlink(); linked.symlink_to(self.receipt) + with self.assertRaises(ValueError): H.receipt_bytes(linked) + for content in (b'', b'x' * 32769): + self.receipt.write_bytes(content) + with self.assertRaises(ValueError): H.receipt_bytes(self.receipt) + + def test_receipt_foreign_uid_and_path_replacement_rejected(self): + original = H.os.fstat + def foreign(fd): + values = list(original(fd)); values[4] += 1 + return os.stat_result(values) + with patch.object(H.os, 'fstat', side_effect=foreign), self.assertRaises(ValueError): + H.receipt_bytes(self.receipt) + original_lstat = Path.lstat + def replaced(path): + if path == self.receipt: + values = list(original_lstat(path)); values[1] += 1 + return os.stat_result(values) + return original_lstat(path) + with patch.object(Path, 'lstat', replaced), self.assertRaises(ValueError): + H.receipt_bytes(self.receipt) + + def test_group_missing_malformed_duplicate_and_linked_counters_rejected(self): + path = self.root / 'group'; counters(path, os.getpid()) + group = H.Group(path, os.getpid()) + try: + for name, content in [('memory.max', 'max\n'), ('memory.peak', '-1\n'), + ('memory.events', 'oom_kill 0\noom_kill 0\n'), + ('memory.events', 'oom 0\n'), ('memory.events', 'oom_kill unknown\n')]: + target = path / name; old = target.read_bytes(); target.write_text(content) + with self.subTest(name=name, content=content), self.assertRaises(ValueError): group.memory() + target.write_bytes(old) + target = path / 'memory.peak'; target.unlink() + with self.assertRaises(FileNotFoundError): group.memory() + target.symlink_to(self.receipt) + with self.assertRaises(OSError): group.memory() + finally: + group.close() + + def test_group_rejects_extra_descendant_or_foreign_members(self): + path = self.root / 'group'; counters(path, os.getpid()) + group = H.Group(path, os.getpid()) + try: + for members in ('', '1\n', f'{os.getpid()}\n1\n'): + (path / 'cgroup.procs').write_text(members) + with self.assertRaises(ValueError): group.memory() + (path / 'cgroup.procs').write_text(str(os.getpid())) + child = path / 'child'; child.mkdir(); (child / 'cgroup.procs').write_text('1\n') + with self.assertRaises(ValueError): group.memory() + finally: group.close() + + def test_group_rejects_replaced_path_or_parent_identity(self): + parent = self.root / 'parent'; parent.mkdir() + path = parent / 'group'; counters(path, os.getpid()) + group = H.Group(path, os.getpid()) + try: + path.rename(parent / 'old'); counters(path, os.getpid()) + with self.assertRaisesRegex(ValueError, 'cgroup_changed'): group.memory() + finally: group.close() + group = H.Group(path, os.getpid()) + try: + parent.rename(self.root / 'old-parent'); parent.mkdir(); counters(path, os.getpid()) + with self.assertRaisesRegex(ValueError, 'cgroup_changed'): group.memory() + finally: group.close() + + def run_parent(self, *, status=0, broken_counter=False, release_error=False, mismatch=False, + receipt_changed=False, interrupted=False, receipt_mode=0o600, malformed=False): + task = dict(version=2, output_constraint=T.OUTPUT_CONSTRAINT, model_profile=T.LARGE_MODEL, passed=status == 0) + self.receipt.write_text('{invalid private fixture' if malformed else json.dumps(task)) + self.receipt.chmod(receipt_mode) + group_path = self.root / 'group' + process = None + starts = [] + def start(name, command, limit, seconds): + nonlocal process + starts.append((name, command, limit, seconds)) + values = dict(zip(command[3::2], command[4::2])) + process = self.spawn(self.root / H.CONTROL, status=status, + nonce=values['--handshake-nonce'], until=int(values['--deadline-monotonic-ns'])) + counters(group_path, process.pid) + if broken_counter: (group_path / 'memory.peak').unlink() + def properties(name): + rc = process.poll() + if rc is None: return dict(ActiveState='active', SubState='running', MainPID=str(process.pid)) + # Reproduce the original systemd lifetime: after its last process + # leaves, the task cgroup may disappear despite RemainAfterExit. + for path in group_path.iterdir(): path.unlink() + group_path.rmdir() + return dict(ActiveState='active', SubState='exited', MainPID='0', + Result='success' if rc == 0 else 'exit-code', ExecMainStatus=str(rc + int(mismatch))) + report = {} + profile = T.trial_profile(T.LARGE_MODEL, BACKEND) + with ExitStack() as stack: + for key, value in dict(BASE=self.root, INPUT=self.root / 'inputs', module=lambda *_: H, + start=start, properties=properties, cgroup=lambda _: group_path, empty=lambda _: process.poll() is not None, + memory=lambda _: dict(maximum=11 * T.GIB, swap=0, peak=123, oom_kill=0)).items(): + stack.enter_context(patch.object(T, key, value)) + if release_error: + stack.enter_context(patch.object(H, 'send_frame', side_effect=OSError('synthetic release'))) + if receipt_changed: + stack.enter_context(patch.object(H, 'receipt_bytes', return_value=b'changed private fixture')) + if interrupted: + stack.enter_context(patch.object(H.Group, 'memory', side_effect=KeyboardInterrupt)) + try: + T.run_native_task(profile, BACKEND, report) + except BaseException as error: + return report, error, process, starts + return report, None, process, starts + + def test_real_parent_socket_group_observation_and_release_chain(self): + report, error, process, starts = self.run_parent() + self.assertIsNone(error) + self.assertEqual(process.wait(timeout=3), 0) + self.assertTrue(report['task']['passed']) + self.assertEqual(report['task_exit_status'], 0) + self.assertEqual(report['task_unit_result'], 'success') + self.assertEqual(report['task_memory']['peak'], 123456) + self.assertFalse((self.root / 'group').exists()) + # Same post-exit observation as the original implementation now fails: + with patch.object(T, 'cgroup', return_value=self.root / 'group'), self.assertRaises(FileNotFoundError): + T.memory(T.TASK_UNIT) + self.assertEqual(starts[0][2:], (768 * 1024**2, 2550)) + + def test_missing_counter_preserves_task_status_but_never_accepts(self): + report, error, process, _ = self.run_parent(broken_counter=True) + self.assertIsInstance(error, FileNotFoundError) + self.assertEqual(report['task_exit_status'], 0) + self.assertTrue(report['task']['passed']) + self.assertNotIn('task_memory', report) + self.assertNotIn('task_unit_result', report) + self.assertEqual(process.wait(timeout=3), 0) + + def test_release_failure_cannot_mask_first_observation_error(self): + report, error, process, _ = self.run_parent(broken_counter=True, release_error=True) + self.assertIsInstance(error, FileNotFoundError) + self.assertNotIn('task_memory', report) + self.assertEqual(process.wait(timeout=3), 1) + + def test_failed_child_survives_parent_counter_failure(self): + report, error, process, _ = self.run_parent(status=17, broken_counter=True) + self.assertIsInstance(error, FileNotFoundError) + self.assertEqual(report['task_exit_status'], 17) + self.assertFalse(report['task']['passed']) + self.assertEqual(process.wait(timeout=3), 17) + + def test_failed_child_with_invalid_receipt_mode_retains_status(self): + report, error, process, _ = self.run_parent(status=17, receipt_mode=0o644) + self.assertIsNone(error) + self.assertEqual(report['task_exit_status'], 17) + self.assertEqual(report['task_unit_result'], 'exit-code') + self.assertNotIn('task', report) + self.assertEqual(process.wait(timeout=3), 17) + # The unchanged guest task acceptance gate still refuses this result. + self.assertFalse(report['task_exit_status'] == 0 and report.get('task', {}).get('passed') is True) + + def test_successful_child_with_invalid_receipt_mode_is_not_accepted(self): + report, error, process, _ = self.run_parent(receipt_mode=0o644) + self.assertIsInstance(error, ValueError) + self.assertEqual(str(error), 'task_holder_missing_success_receipt') + self.assertEqual(report['task_exit_status'], 0) + self.assertNotIn('task', report) + self.assertNotIn('task_unit_result', report) + self.assertEqual(process.wait(timeout=3), 1) + + def test_failed_child_with_malformed_json_retains_status(self): + report, error, process, _ = self.run_parent(status=17, malformed=True) + self.assertIsInstance(error, json.JSONDecodeError) + self.assertEqual(report['task_exit_status'], 17) + self.assertNotIn('task', report) + self.assertNotIn('task_unit_result', report) + self.assertEqual(process.wait(timeout=3), 17) + + def test_successful_child_with_malformed_json_is_not_accepted(self): + report, error, process, _ = self.run_parent(malformed=True) + self.assertIsInstance(error, json.JSONDecodeError) + self.assertEqual(report['task_exit_status'], 0) + self.assertNotIn('task', report) + self.assertNotIn('task_unit_result', report) + # Holder exit preserves the actual successful child exit, but the + # parent rejected the receipt and never accepted a completed trial. + self.assertEqual(process.wait(timeout=3), 0) + + def test_terminal_unit_must_match_joined_child(self): + report, error, process, _ = self.run_parent(mismatch=True) + self.assertIsInstance(error, ValueError) + self.assertEqual(str(error), 'task_holder_exit') + self.assertEqual(process.wait(timeout=3), 0) + + def test_receipt_hash_mismatch_releases_holder_but_refuses_task(self): + report, error, process, _ = self.run_parent(receipt_changed=True) + self.assertIsInstance(error, ValueError) + self.assertEqual(str(error), 'task_receipt_changed') + self.assertNotIn('task', report) + self.assertNotIn('task_memory', report) + self.assertEqual(process.wait(timeout=3), 0) + + def test_owner_interruption_releases_holder_without_accepting(self): + report, error, process, _ = self.run_parent(interrupted=True) + self.assertIsInstance(error, KeyboardInterrupt) + self.assertNotIn('task_memory', report) + self.assertNotIn('task_unit_result', report) + self.assertEqual(process.wait(timeout=3), 0) + + def test_release_failure_after_valid_counters_still_refuses_completion(self): + report, error, process, _ = self.run_parent(release_error=True) + self.assertIsInstance(error, OSError) + self.assertIn('task_memory', report) + self.assertNotIn('task_unit_result', report) + self.assertEqual(process.wait(timeout=3), 1) + + def test_wiring_native_only_fixed_command_original_caps_and_cleanup(self): + parent = inspect.getsource(T.run_native_task) + self.assertIn('started + 2550 * 1_000_000_000', parent) + self.assertIn('started + 2565 * 1_000_000_000', parent) + self.assertEqual(parent.count('start(TASK_UNIT,'), 1) + self.assertNotIn('task_arguments(', parent) + main = inspect.getsource(H.main) + self.assertIn("choices=('llama_cpp_bf16_v1',)", main) + self.assertIn("trial.task_arguments(args.inference_backend)", main) + self.assertIn("trial.BASE / 'task.json'", main) + self.assertIn("'volparossa-alpha'", main) + self.assertIn("'vpci'", main) + self.assertIn('os.kill(os.getpid(), -status)', main) + self.assertIn("'scripts/opencode_task_holder.py'", inspect.getsource(T.pack)) + guest = inspect.getsource(T.guest) + self.assertIn("if backend != 'torch':\n run_native_task(profile, backend, report)\n else:", guest) + self.assertIn("require(report['task_exit_status'] == 0 and report.get('task', {}).get('passed') is True", guest) + final = guest.split(' finally:', 1)[1] + self.assertIn("unit(name, 'stop', check=False)", final) + self.assertIn('empty(name)', final) + # No command/path/limit CLI parameter was introduced in the helper. + parser = ast.parse(main) + options = [node.args[0].value for node in ast.walk(parser) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) and node.func.attr == 'add_argument'] + self.assertEqual(set(options), {'--deadline-monotonic-ns', '--handshake-nonce', '--parent-pid', '--inference-backend'}) + + def test_final_cleanup_preserves_live_native_task_sample_without_skipping_stop(self): + tree = ast.parse(inspect.getsource(T.guest)) + condition = next(node.value for node in ast.walk(tree) if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == 'observed_task' + for target in node.targets)) + for backend in ('torch', BACKEND): + for name in (T.TASK_UNIT, T.CORE_UNIT, T.NATIVE_UNIT): + for observed in (False, True): + skip = eval(compile(ast.Expression(condition), '', 'eval'), + dict(backend=backend, name=name, TASK_UNIT=T.TASK_UNIT, + report={'task_memory': {}} if observed else {})) + self.assertEqual(skip, backend == BACKEND and name == T.TASK_UNIT and observed) + # Stop remains outside the observation guard; a missing observation is + # not replaced with zero and cannot skip the original cleanup. + units = next(node for node in ast.walk(tree) if isinstance(node, ast.For) + and isinstance(node.iter, ast.Call) and isinstance(node.iter.func, ast.Name) + and node.iter.func.id == 'reversed') + self.assertIsInstance(units.body[-1], ast.Expr) + self.assertEqual(units.body[-1].value.func.id, 'unit') + + +if __name__ == '__main__': + unittest.main() From 8e873c3d1b38c5fab25b6d336aef0dddb1bd2501 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:32:57 +0200 Subject: [PATCH 37/39] docs: distinguish completed coding task from pending full acceptance --- README.md | 34 ++++++++++-------- docs/OPENCODE.md | 89 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 76859df..01ad120 100644 --- a/README.md +++ b/README.md @@ -63,23 +63,29 @@ not Codex. It joins these implemented components: - An explicit Linux launcher with a selected writable project, temporary state, no inherited account credentials and no external network interface. -**Current proof:** the pinned OpenCode source builds and the actual runtime +**Runtime and adapter proof:** the pinned OpenCode source builds and the actual runtime completes a tool loop through the production launcher and adapters: one approved command changes a disposable file, its tool result returns to the core interface, and the session shuts down cleanly. A second native trial invokes the cooperative tool, preserves complete and incomplete core results, and confirms that only the enrolled public snapshot crosses the bridge. Both trials use **synthetic core/model replies**, not real inference or peer execution. Focused checks additionally cover -adapter, editor and lifecycle behavior. The complete model-driven OpenCode -read/edit/test loop, native editor UI operation, protected peer execution and a -finished immune-policy path remain unproved. - -**Latest real native CPU trial:** the [4B run](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38742699743) -completed two OpenCode `read` actions, then reached the third model request's -compute deadline. The project remained unchanged; no edit or test passed. -The next candidate reuses the model and matching prompt prefixes within one -owner task, with the ordinary toolset and bounded resources. Its transport tests -do not yet prove faster inference or a complete coding task. See the +adapter, editor and lifecycle behavior. + +**Real model-driven coding:** the [Qwen4B native CPU trial](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38806073909) +completed two reads, an approved edit and an approved test through actual +OpenCode and core inference. The original tests stayed unchanged and an +independent check passed. The bounded coding task took about 37 minutes; this +does not establish competitive performance or general coding quality. + +The **whole trial still failed** during its final checks: task-resource +measurements and the later input-integrity and graceful core-stop results were +not retained. Runtime and owned guest cleanup were confirmed separately. The +follow-up keeps the task's resource counters available until the parent has +verified them, without changing the model, ordinary tools, permissions or +budgets. Its source checks pass; the [new complete trial](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38816006720) +has not yet been accepted. Native editor UI operation, protected peer execution +and the complete immune-policy path also remain unproved. See the [native CPU evidence](docs/OPENCODE.md#explicit-native-cpu-candidate). **Real public peer proof:** a separate owner command successfully obtained a @@ -146,9 +152,9 @@ notice and source pin are preserved in [third-party provenance](THIRD_PARTY_LICE ## Remaining work -- Exercise the source-built OpenCode runtime with actual core inference and a - model-driven read/edit/test task with explicit local approvals and independently - checked results, then verify native editor operation. +- Complete the whole real-model trial, including task-resource measurements, + input-integrity checks and graceful core shutdown after the proven bounded + read/edit/test task, then verify native editor operation. - Extend the proven public single-file owner command to a complete real-model OpenCode cooperative tool loop; retain original results, cancellation and provenance. - Implement remote conversation execution and actual protected private work, diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index aaeaecf..f5d3ca6 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -4,6 +4,12 @@ The selected foundation is OpenCode v1.18.34 at `aec0b9a6d8898f68f923aaf08b7306d931fd9d76`. Codex scripts and reports are historical, not an alternative implicitly selected by the editor. +The native Qwen4B path has completed a bounded model-driven read/edit/test task, +but the enclosing trial failed during its final checks. The corrected full +trial has not yet been accepted. See [current native CPU evidence](#explicit-native-cpu-candidate) +for the exact sources and remaining acceptance requirements. Historical trial +records below retain their original pins and failed outcomes. + ## Explicit source build On Linux amd64, with Python 3 and bubblewrap already installed: @@ -81,8 +87,9 @@ Limits remain 600 seconds per request, at most 128 requests, 300 idle seconds an covers that task lifetime; each request retains its own CPU allowance and the existing pressure and memory checks. Tool descriptions, context limits, model, workspace permissions and independent task checks are not reduced for the trial. -Synthetic Unix/HTTP tests verify the new correlation and cleanup contracts; -actual model reuse, acceleration and successful editing remain to be demonstrated. +Synthetic Unix/HTTP tests verify the correlation and cleanup contracts. The +[native task](#explicit-native-cpu-candidate) subsequently completed real reads, +editing and testing; acceleration has not been established by a comparison. This private-local execution slice does not complete protected peer execution. ### Core-selected coding profiles @@ -105,20 +112,20 @@ execution on other peers. 76 targeted Node contract tests and ten inert Python session-selection tests pass. They cover actual HTTP/Unix framing with synthetic core replies, legacy compatibility, result binding, namespace command selection and startup refusal. -No model or OpenCode binary ran in those tests. The core's source-only normal -build now passes all five native check targets; ABI, loader, BF16/F32 and -output-constraint checks also pass under ASan/UBSan. The wider upstream tensor -test still fails on its previously recorded Q1_0/Q8_0 alignment error. See +No model or OpenCode binary ran in those tests. At that historical checkpoint, +the core's source-only normal build passed all five native check targets; ABI, +loader, BF16/F32 and output-constraint checks also passed under ASan/UBSan. The +wider upstream tensor test failed on its Q1_0/Q8_0 alignment error. See [the original source-only run](https://github.com/VOLPAROSSA/volparossa/actions/runs/38671551374). The earlier invalid JSON trial is not considered fixed by these contract checks. -The next native trial is pinned to core +That historical native candidate was pinned to core `5607f1177dc230a71414e7468c221b950f43bca6`, with matching build v3/backend v2 provenance and an explicit selector passed into the actual OpenCode runtime. The native guest and outer verifier require task receipt v2 with that selector; legacy Torch arguments and task receipt v1 remain unchanged. -The last completed trial on core `9d044921` reached one actual OpenCode read, +The earlier trial on core `9d044921` reached one actual OpenCode read, then stopped without an edit or test; its six execution failures lacked an identified cause. The new pin retains closed failure codes at the private-task boundary, including admission before worker startup and result validation after @@ -149,9 +156,9 @@ admission. Model quality and useful execution still require real-model evidence. Before starting OpenCode, the owner validates the complete core capability reply, including template, limits, private-local scope and negotiated `greedy_v1` policy. Current native sessions also require `execution_error_version: 1`; an older core -without that capability is refused before starting the runtime. Both native smoke -profiles pin core `6a517b576baa17e7329661ee0476d1848081d114` for this contract, -including the preserved main integrations and reviewed source-quality fixes. +without that capability is refused before starting the runtime. Both legacy Torch +smoke profiles retain core `6a517b576baa17e7329661ee0476d1848081d114`. +The native CPU backend has its own [current pin](#explicit-native-cpu-candidate). Legacy Q&A and conversation clients that do not opt in remain unchanged. That one model identity binds the native catalog, all agent roles, provider, @@ -481,9 +488,11 @@ the report explicitly records `model_inference: false` and `peer_execution: fals ``` Its `native-cooperative-smoke-report.json` is separate from the local-tool report. -Neither report may be relabelled as real inference or immune-policy proof. -Actual model-driven coding, native editor UI operation, confidential peer -execution and full immune supervision remain unproved. +Neither synthetic report may be relabelled as real inference or immune-policy +proof. A separate [real native task](#explicit-native-cpu-candidate) now proves +bounded model-driven coding, with whole-trial acceptance still pending. Native +editor UI operation, confidential peer execution and full immune supervision +remain unproved. ## Real public-core integration driver @@ -581,9 +590,10 @@ stop, and greedy generation does not guarantee a completed coding task. `scripts/smoke_opencode_inference.py` prepares one explicit disposable Debian 13 KVM trial; it does not install or run the model on the development host. Its `pack` mode captures each Code source/runtime file by hash and the exact core -archive selected by its explicit model profile. Both current profiles pin core +archive selected by its explicit model profile and backend. Both Torch profiles pin core `6a517b576baa17e7329661ee0476d1848081d114`, while retaining their different -models and resource profiles. Historical 0.6B trials used `845cc84d`; the last +models and resource profiles. The native backend uses the separate pin below. +Historical 0.6B trials used `845cc84d`; an earlier 4B trial used `1297f8f1a5d163d802efd066c51a950b95588fa5`, including the bounded provisioning-timeout recovery from `3aa0e2d0`. Neither those historical outcomes nor the synthetic native-runtime probe proves the current real-model candidate. @@ -787,6 +797,53 @@ launch a guest. There is no automatic inference run on push or pull request. ### Explicit native CPU candidate +The native `llama_cpp_bf16_v1` backend currently pins core +`6d65bb12ed7b8720d43868c9306313133a5b96ae` and llama.cpp source +`7fe450e19305b828c199d602c23a8337aaa1f03b`, with the original Qwen4B BF16 model. +It uses a resident connection-bound session, build-manifest v4, backend-manifest +v3 and the explicitly selected `qwen_tagged_tool_json_v1` output constraint. + +In [trial 38806073909](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38806073909), +Code `cce5a211a0624fdcc05a787bd4ff31422b30ae65` and that core completed +**the bounded coding task, not the whole trial**. Five real model results +produced four tool calls and a final response: two reads, one approved edit and +one approved bash test. The original test remained unchanged, the independent +check passed, and no model answers were injected. The task took 2,236,512 ms, +about 37 minutes. All five request cleanups and runtime/project cleanup passed. + +The enclosing trial failed with `stage_failed` after the task. Task-memory +measurements and the later `inputs_unchanged` and `core_clean_stop` fields are +absent, not passing or zero observations. Core peak memory was 6,918,348,800 bytes +within 11 GiB, with zero swap and OOM kills. Owned guest units, private data, +QEMU and scratch cleanup passed; normalized routes/DNS matched. These results +do not prove confidential peer execution or general coding performance. + +Original artifact ZIP SHA-256: +`8c31054298ef59f0a50c360d4f772afaa010b3c66144e4af85013f49d8565925`; +original log ZIP SHA-256: +`5e21e3f3ec2b6310c58079e81eacb1b35a2cf06858650afa8176463196804d30`. + +Source review found that the parent read task-resource counters after the command +exited, when its empty cgroup could already have disappeared. The generic original +failure does not prove that this was the particular failing read. The correction keeps a small holder +alive after joining the unchanged task command. A nonce-bound local socket, +peer credentials, receipt hash and original exit status bind the handshake. The +parent checks the exact cgroup and reads its actual counters before releasing +the holder, then requires the original status and an empty unit. Missing +counters remain errors. The same absolute deadlines include this handshake. + +[Source checks](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38815603511) +pass 533 distinct tests with five explicit skips, including all 29 holder tests. +The corrected [full trial 38816006720](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38816006720) +is bound to Code `0031c0e97f8bd0221c207c5a0f00370073cf617d` and the same core, +model, ordinary tools, permissions and budgets. Its whole-trial acceptance is +still pending; source checks do not supply missing execution evidence. + +#### Earlier native CPU trials + +The following records describe their original candidates, not the current pin. +Their failures remain failures even when a later source correction passes. + The later original [4B trial 37228603308](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/37228603308) on Code `714904a3ec452b8e10d8b40be6ac85107606ca3b` and core `6a517b576baa17e7329661ee0476d1848081d114` remains **failed**. One request From fd4ff9688c58d6228e07f2116a7e8dcdb12a89ef Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Fri, 16 Oct 2026 01:30:08 +0200 Subject: [PATCH 38/39] fix(code): pin adaptive prefill core for native coding trial --- README.md | 21 ++++++++++------- docs/OPENCODE.md | 35 ++++++++++++++++++++++++----- scripts/smoke_opencode_inference.py | 2 +- tests/test_opencode_native_cpu.py | 2 +- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 01ad120..516178d 100644 --- a/README.md +++ b/README.md @@ -78,14 +78,19 @@ OpenCode and core inference. The original tests stayed unchanged and an independent check passed. The bounded coding task took about 37 minutes; this does not establish competitive performance or general coding quality. -The **whole trial still failed** during its final checks: task-resource -measurements and the later input-integrity and graceful core-stop results were -not retained. Runtime and owned guest cleanup were confirmed separately. The -follow-up keeps the task's resource counters available until the parent has -verified them, without changing the model, ordinary tools, permissions or -budgets. Its source checks pass; the [new complete trial](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38816006720) -has not yet been accepted. Native editor UI operation, protected peer execution -and the complete immune-policy path also remain unproved. See the +The **whole trial still failed** during its final checks; required task-resource +measurements and later checks were not retained. The +[follow-up trial](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38816006720) +retains the task status and memory counters, but encounters a different failure: +after two reads and an edit, the worker misses a pause acknowledgement. No test +command completes in that run. + +The next candidate processes the complete model prompt in adaptively sized +blocks, allowing more frequent safe pause checkpoints. The model, ordinary +tools, permissions, budgets and ten-second pause guard are unchanged. Focused +source checks pass; real responsiveness and complete execution still need a new +trial. Native editor UI operation, protected peer execution and the complete +immune-policy path also remain unproved. See the [native CPU evidence](docs/OPENCODE.md#explicit-native-cpu-candidate). **Real public peer proof:** a separate owner command successfully obtained a diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index f5d3ca6..4c66757 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -798,13 +798,14 @@ launch a guest. There is no automatic inference run on push or pull request. ### Explicit native CPU candidate The native `llama_cpp_bf16_v1` backend currently pins core -`6d65bb12ed7b8720d43868c9306313133a5b96ae` and llama.cpp source +`7d0f9f0b492c5e692cc9d26f23d95f55f2c1e651` and llama.cpp source `7fe450e19305b828c199d602c23a8337aaa1f03b`, with the original Qwen4B BF16 model. It uses a resident connection-bound session, build-manifest v4, backend-manifest v3 and the explicitly selected `qwen_tagged_tool_json_v1` output constraint. In [trial 38806073909](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38806073909), -Code `cce5a211a0624fdcc05a787bd4ff31422b30ae65` and that core completed +Code `cce5a211a0624fdcc05a787bd4ff31422b30ae65` and core +`6d65bb12ed7b8720d43868c9306313133a5b96ae` completed **the bounded coding task, not the whole trial**. Five real model results produced four tool calls and a final response: two reads, one approved edit and one approved bash test. The original test remained unchanged, the independent @@ -835,9 +836,33 @@ counters remain errors. The same absolute deadlines include this handshake. [Source checks](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38815603511) pass 533 distinct tests with five explicit skips, including all 29 holder tests. The corrected [full trial 38816006720](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38816006720) -is bound to Code `0031c0e97f8bd0221c207c5a0f00370073cf617d` and the same core, -model, ordinary tools, permissions and budgets. Its whole-trial acceptance is -still pending; source checks do not supply missing execution evidence. +is bound to Code `0031c0e97f8bd0221c207c5a0f00370073cf617d` and core `6d65bb12`, +with the same model, ordinary tools, permissions and budgets. Its holder +retains task exit status 1 and actual memory counters: peak 740,012,032 bytes +within 768 MiB, zero swap and OOM kills. This task instead fails on +`compute_control_ack_deadline` after two reads and an approved edit, before +any test. Eight controls were issued and seven acknowledged, leaving a pause +outstanding. All four requests confirm cleanup; guest units and private data +are removed. Later success-only input and graceful core-stop checks remain +absent. The record does not identify a particular slow native call. + +Original artifact and log ZIP SHA-256 values are respectively +`06ce2a9e821df25a28551f1160ca686acb8adac722637ee0537d692b2c7da83c` +and `e97b9384a917831904490fa9b805629a12dd533cb6fee80878bd98f237622ba7`. + +The current core pin adds adaptive prefill checkpoints: begin with 16 tokens, +then adapt within 1–128 tokens using synchronized decode duration, excluding +owner-requested pause time. All prompt tokens and retained-prefix rules remain; +acknowledgement still follows joined native work. The original ten-second +guard fails closed if even one token stalls. This is not preemption or a +latency guarantee, and changed batching need not produce bit-identical output. + +All 116 core inert contracts pass, including 18 new scheduling cases. Code's +pin integration passes 88 focused cases with one explicit optional patched-loader +fixture skip; the actual pinned core imports and CPU-overlay checks pass. +Native C++, builder/converter, IPC, supervisor and resource limits are unchanged. +The older Torch pins and golden encodings remain intact. No new actual coding +acceptance or native performance result is established by these source checks. #### Earlier native CPU trials diff --git a/scripts/smoke_opencode_inference.py b/scripts/smoke_opencode_inference.py index f943806..2a07d91 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -36,7 +36,7 @@ MODEL_PROFILES = (MODEL, LARGE_MODEL) INFERENCE_BACKENDS = ('torch', 'llama_cpp_bf16_v1') # Native validation, output constraints and closed task diagnostics are source-bound. -NATIVE_CORE = '6d65bb12ed7b8720d43868c9306313133a5b96ae' +NATIVE_CORE = '7d0f9f0b492c5e692cc9d26f23d95f55f2c1e651' NATIVE_SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1'} diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index 46049df..ad35351 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -37,7 +37,7 @@ def load(name): class Contracts(unittest.TestCase): def test_native_is_refused_without_final_pin_and_cannot_replace_default(self): - self.assertEqual(TRIAL.NATIVE_CORE, '6d65bb12ed7b8720d43868c9306313133a5b96ae') + self.assertEqual(TRIAL.NATIVE_CORE, '7d0f9f0b492c5e692cc9d26f23d95f55f2c1e651') original = TRIAL.trial_profile(TRIAL.LARGE_MODEL) self.assertNotIn('inference_backend', original) with patch.object(TRIAL, 'NATIVE_CORE', None), self.assertRaisesRegex(ValueError, 'native_core_not_pinned'): From 32d7a084b3680e720ca1e1420f99cd5e9e2a813d Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:37:31 +0200 Subject: [PATCH 39/39] docs: record accepted native coding lifecycle --- README.md | 39 ++++++++++++++++------------------- docs/OPENCODE.md | 53 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 516178d..25aa507 100644 --- a/README.md +++ b/README.md @@ -72,25 +72,21 @@ enrolled public snapshot crosses the bridge. Both trials use **synthetic core/mo replies**, not real inference or peer execution. Focused checks additionally cover adapter, editor and lifecycle behavior. -**Real model-driven coding:** the [Qwen4B native CPU trial](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38806073909) -completed two reads, an approved edit and an approved test through actual -OpenCode and core inference. The original tests stayed unchanged and an -independent check passed. The bounded coding task took about 37 minutes; this -does not establish competitive performance or general coding quality. - -The **whole trial still failed** during its final checks; required task-resource -measurements and later checks were not retained. The -[follow-up trial](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38816006720) -retains the task status and memory counters, but encounters a different failure: -after two reads and an edit, the worker misses a pause acknowledgement. No test -command completes in that run. - -The next candidate processes the complete model prompt in adaptively sized -blocks, allowing more frequent safe pause checkpoints. The model, ordinary -tools, permissions, budgets and ten-second pause guard are unchanged. Focused -source checks pass; real responsiveness and complete execution still need a new -trial. Native editor UI operation, protected peer execution and the complete -immune-policy path also remain unproved. See the +**Real model-driven coding:** the [Qwen4B native CPU trial](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38824828727) +passes the bounded coding task **and its complete lifecycle**. Actual OpenCode +and core inference produce two reads, an approved edit, an approved test and +a final response. The original tests stay unchanged and an independent check +passes. Task-resource measurements, unchanged inputs, clean core shutdown and +guest/VM cleanup are verified. + +The task takes about 37 minutes on the disposable CPU runner; this is a working +development slice, not competitive performance or general coding-quality proof. +The core processes the complete prompt in adaptive blocks while retaining the +model, permissions, resource budgets and ten-second owner-control guard. Earlier +failed trials remain documented. This path pins the exact tested core candidate +`7d0f9f0b`, rather than selecting whichever core happens to be on `main`. +Native editor UI operation, protected peer execution and the complete +immune-policy path remain open. See the [native CPU evidence](docs/OPENCODE.md#explicit-native-cpu-candidate). **Real public peer proof:** a separate owner command successfully obtained a @@ -157,9 +153,8 @@ notice and source pin are preserved in [third-party provenance](THIRD_PARTY_LICE ## Remaining work -- Complete the whole real-model trial, including task-resource measurements, - input-integrity checks and graceful core shutdown after the proven bounded - read/edit/test task, then verify native editor operation. +- Verify native editor operation with the proven real-model read/edit/test + lifecycle, and improve measured coding quality, speed and resource use. - Extend the proven public single-file owner command to a complete real-model OpenCode cooperative tool loop; retain original results, cancellation and provenance. - Implement remote conversation execution and actual protected private work, diff --git a/docs/OPENCODE.md b/docs/OPENCODE.md index 4c66757..0b3f4f8 100644 --- a/docs/OPENCODE.md +++ b/docs/OPENCODE.md @@ -4,11 +4,11 @@ The selected foundation is OpenCode v1.18.34 at `aec0b9a6d8898f68f923aaf08b7306d931fd9d76`. Codex scripts and reports are historical, not an alternative implicitly selected by the editor. -The native Qwen4B path has completed a bounded model-driven read/edit/test task, -but the enclosing trial failed during its final checks. The corrected full -trial has not yet been accepted. See [current native CPU evidence](#explicit-native-cpu-candidate) -for the exact sources and remaining acceptance requirements. Historical trial -records below retain their original pins and failed outcomes. +The native Qwen4B path passes a bounded model-driven read/edit/test task and its +complete guest/VM lifecycle. See [current native CPU evidence](#explicit-native-cpu-candidate) +for the exact tested core candidate and remaining editor, peer-execution and +quality work. Historical trial records below retain their original pins and +failed outcomes. ## Explicit source build @@ -490,7 +490,7 @@ the report explicitly records `model_inference: false` and `peer_execution: fals Its `native-cooperative-smoke-report.json` is separate from the local-tool report. Neither synthetic report may be relabelled as real inference or immune-policy proof. A separate [real native task](#explicit-native-cpu-candidate) now proves -bounded model-driven coding, with whole-trial acceptance still pending. Native +bounded model-driven coding and its complete local execution lifecycle. Native editor UI operation, confidential peer execution and full immune supervision remain unproved. @@ -803,6 +803,47 @@ The native `llama_cpp_bf16_v1` backend currently pins core It uses a resident connection-bound session, build-manifest v4, backend-manifest v3 and the explicitly selected `qwen_tagged_tool_json_v1` output constraint. +#### Accepted bounded coding lifecycle + +[Original trial 38824828727](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38824828727) +passes on Code `fd4ff9688c58d6228e07f2116a7e8dcdb12a89ef` and that exact core +candidate. All five native requests complete: four tool calls (two reads, +one approved edit and one approved bash test), then a final assistant response. +The independent three-case checker passes, the original tests remain unchanged, +and no synthetic core or injected model answer supplies the result. The task +takes 2,219,455 ms, about 37 minutes. + +The task exits successfully and retains its actual memory counters: peak +740,847,616 bytes within 768 MiB. Core peak is 6,912,872,448 bytes within 11 GiB; +both report zero swap and OOM kills. The outer QEMU cgroup reaches its memory +limit and records memory-max events, but no OOM or OOM kill. This is not evidence +of pressure-free execution or competitive performance. + +All request cleanups, separate runtime/session closure, project removal, +unchanged guest inputs, empty guest units and graceful core shutdown pass. +QEMU joins, its scratch space is removed and job-owned host changes are restored. +The observed host route/DNS configuration matches after normalization; this +does not cover a full host firewall inventory. + +Source-bound per-turn validation checks correlated worker-control +acknowledgements under the unchanged guard. No per-acknowledgement counts or +pause latencies are exported, so neither is claimed. All 35 packed Code source +files match the tested commit, the complete core archive matches `7d0f9f0b`, +and the input manifest and native wrapper/build/model provenance are verified. +The legacy `code_contains_uncommitted_changes: true` receipt field is a +hardcoded packaging label, not a measured dirty checkout; the independent +source binding and CI clean-checkout check establish the actual inputs. + +Original artifact and log ZIP SHA-256 values are respectively +`abf57ddb609c631746d1d9bcd532a17e73c52d5a321f096eaef3e6a713bf3abe` +and `18098c557bb8a56caa78c2a7823c9007bd71474e85f687595c8339d944943ab4`. +This accepts the bounded local coding lifecycle, not native editor UI, +protected private-peer execution or general coding quality. The core dependency +remains pinned to the tested candidate while its separate Storage integration +continues; core `main` is not an interchangeable dependency. + +#### Earlier failures and the prefill correction + In [trial 38806073909](https://github.com/VOLPAROSSA/volparossa-code/actions/runs/38806073909), Code `cce5a211a0624fdcc05a787bd4ff31422b30ae65` and core `6d65bb12ed7b8720d43868c9306313133a5b96ae` completed