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/.github/workflows/source-checks.yml b/.github/workflows/source-checks.yml index c2750c0..3584f35 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 @@ -27,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/README.md b/README.md index 2ac7c70..25aa507 100644 --- a/README.md +++ b/README.md @@ -63,16 +63,31 @@ 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. +adapter, editor and lifecycle behavior. + +**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 small single-file replacement from a real Qwen0.6B worker on another peer. @@ -138,9 +153,8 @@ 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. +- 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 a9031d6..0b3f4f8 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 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 On Linux amd64, with Python 3 and bubblewrap already installed: @@ -60,8 +66,82 @@ 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 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 +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. 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. + +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 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 +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. - `qwen3-4b-instruct-2507-v1`: the explicit larger profile; native template @@ -75,6 +155,12 @@ 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 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, task/session checks and trial receipt. Each provider request checks the core again; a different profile or incompatible result is refused, not silently substituted. @@ -108,17 +194,24 @@ 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. 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 @@ -395,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 and its complete local execution lifecycle. Native +editor UI operation, confidential peer execution and full immune supervision +remain unproved. ## Real public-core integration driver @@ -438,7 +533,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 @@ -458,9 +553,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) @@ -473,12 +590,25 @@ 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 and backend. Both Torch profiles pin core +`6a517b576baa17e7329661ee0476d1848081d114`, while retaining their different +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. + +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, but the native task failed at `invalid_output` after 152,995 ms. No tools, edits @@ -659,8 +789,448 @@ 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 + +The native `llama_cpp_bf16_v1` backend currently pins core +`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. + +#### 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 +**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 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 + +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 +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 +`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. + +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. + +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. + +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. + +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. + +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. + +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 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 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 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 subsequent source correction used 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. 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 +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. + +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, +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. +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`. + +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`. diff --git a/scripts/opencode_ci.py b/scripts/opencode_ci.py index 23b4647..c995def 100644 --- a/scripts/opencode_ci.py +++ b/scripts/opencode_ci.py @@ -25,13 +25,15 @@ 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'), '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): @@ -75,14 +77,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 +100,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 +123,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 +162,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 +194,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 +206,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(): @@ -229,8 +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) <= (observer.MAX_JSON if name == 'qemu-memory.json' else 256 * 1024), + 'closed_receipt_file') record(output / name, value) @@ -240,20 +261,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 +289,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_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/scripts/opencode_native_cpu.py b/scripts/opencode_native_cpu.py new file mode 100644 index 0000000..7cf2d32 --- /dev/null +++ b/scripts/opencode_native_cpu.py @@ -0,0 +1,315 @@ +#!/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 struct +import subprocess +import sys +import time + +SOURCE = '7fe450e19305b828c199d602c23a8337aaa1f03b' +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',) +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') +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'} + + +def require(value, reason): + if not value: + raise ValueError(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) + 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 + + +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()) + 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) + builder.native.validate_build_provenance(value, require) + 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' + 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') + # 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') + 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=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 + + +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(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') == 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'] == 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') + 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') + # Closed hashes/sizes only: no host paths, prompts, source code or model text. + 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=value['version'], build_version=expected_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 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. + """ + 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') + # 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): + 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(), + '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=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) + # 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=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.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: + 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/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/opencode_session.cjs b/scripts/opencode_session.cjs index 1bb3fbe..62b44ff 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,9 +44,12 @@ 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 nativeSession = false, providerClosing; + const closeProvider = () => providerClosing ??= provider.close(); let seq = 0, verificationSeq = 0, pendingApproval, pendingVerification, finish; const expiredApprovals = new Set(); const expiredVerifications = new Set(); @@ -116,10 +119,14 @@ async function runSession({input, output, events = process}, hooks = {}) { 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; @@ -135,20 +142,24 @@ 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, + ...(outputConstraint !== undefined ? {outputConstraintVersion: 1} : {})}); try { return await core.connect(); } finally { core.close(); } - }))(), 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; + 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}); + 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}); @@ -183,7 +194,7 @@ async function runSession({input, output, events = process}, hooks = {}) { client?.close(); if (provider) { try { - await provider.close(); + await closeProvider(); if (provider.observations.submitted !== provider.observations.cleanup_confirmed) bad = true; } catch { bad = true; } } @@ -204,7 +215,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..d0d605c 100644 --- a/scripts/opencode_session.py +++ b/scripts/opencode_session.py @@ -14,7 +14,8 @@ PIN = 'aec0b9a6d8898f68f923aaf08b7306d931fd9d76' FIELDS = {'version', 'opencode', 'opencodeSha256', 'buildReport', 'node', 'nodeSha256', 'socketPath'} COOPERATIVE_FIELD = 'cooperativeSocketPath' -SOURCES = ('private-compute.cjs', 'private-conversation.cjs', 'responses-provider.cjs', +OUTPUT_CONSTRAINT = 'qwen_tagged_tool_json_v1' +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') @@ -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/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/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/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 d9ff175..2a07d91 100644 --- a/scripts/smoke_opencode_inference.py +++ b/scripts/smoke_opencode_inference.py @@ -27,13 +27,18 @@ import uuid ROOT = Path(__file__).resolve().parents[1] -CORE = '845cc84d0d0b766ab1c5227231dbf6c8eaeb8cc3' +CORE = '6a517b576baa17e7329661ee0476d1848081d114' 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 = '6a517b576baa17e7329661ee0476d1848081d114' 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 = '7d0f9f0b492c5e692cc9d26f23d95f55f2c1e651' +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' @@ -43,6 +48,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 +56,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 +78,33 @@ 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 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() @@ -89,7 +128,55 @@ def load(path, maximum=2 * 1024**2): return json.loads(path.read_bytes()) -def closed_provision(path, pins, stage, returncode, wait_timeout=False): +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) + 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'} value = dict(version=1, stage=stage if stage in stages else 'unknown', @@ -97,6 +184,8 @@ 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 + lines = [] try: descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) with os.fdopen(descriptor, 'rb') as stream: @@ -127,6 +216,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 +255,10 @@ 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' + if native_cpu is True: + value['native_conversion'] = closed_conversion(lines, value['log_state'], value['process_status']) return value @@ -187,7 +281,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 +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_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'] @@ -228,6 +323,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 +343,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 +362,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 +374,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 +391,236 @@ 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', + 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) + 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 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) 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 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): - 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 +638,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 +656,23 @@ 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) + 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') - 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 +692,10 @@ 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']) + native.require_output_constraint(report['native_build'], report['native_backend']) provision_stage = 'complete' report['phase'] = 'core-start' (BASE / 'work').mkdir(mode=0o700) @@ -385,7 +703,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) @@ -394,29 +714,30 @@ 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) - 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) - require(report['task']['model_profile'] == profile['model_profile'], 'task_model_mismatch') - 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')) + 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') 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,19 +757,27 @@ 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(): + # 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 '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) 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) @@ -560,8 +889,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 +903,30 @@ 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 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) require(path.stat().st_size < 512 * 1024**2, 'bundle_bound') with tarfile.open(path) as archive: @@ -589,6 +940,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 +955,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 +977,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') @@ -647,6 +1000,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, @@ -655,6 +1009,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 +1064,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', @@ -728,6 +1083,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: @@ -749,10 +1105,16 @@ 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) + 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') + 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 @@ -762,6 +1124,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: @@ -816,6 +1180,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/src/chat-completions-provider.cjs b/src/chat-completions-provider.cjs index 0095575..b475104 100644 --- a/src/chat-completions-provider.cjs +++ b/src/chat-completions-provider.cjs @@ -9,10 +9,11 @@ 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'); +const { PrivateNativeSession } = require('./private-native-session.cjs'); const HTTP_BYTES = 524288; const EXECUTION = Object.freeze({ scope: 'private_local', distributed: false, @@ -66,7 +67,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,13 +151,15 @@ 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; } 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'; @@ -173,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) { @@ -211,13 +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 }) { +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(); @@ -226,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 ?? ''); @@ -239,10 +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 }); - 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()); @@ -250,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(); - const conversation = toConversation(requestBody, model, caps); + 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); @@ -276,25 +318,42 @@ 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'); } 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; + 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; } })(); @@ -319,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/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/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/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/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/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..338e163 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,11 +64,23 @@ 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, 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]); + max_request_bytes: requestLimit(value.model_profile), max_response_bytes: 65536 }, + ['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))) { + 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) { @@ -76,11 +89,20 @@ 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'); + 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) && @@ -90,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'); @@ -131,19 +159,33 @@ 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'], - 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'); } - 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 (Object.hasOwn(input, 'output_constraint')) { + validateConversation(input, caps); + check(value.output_constraint === input.output_constraint, 'output_constraint_mismatch'); + } + 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 && @@ -167,11 +209,22 @@ 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 } = {}) { + 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. @@ -179,7 +232,9 @@ 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 } : {}), + ...(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'); @@ -203,18 +258,24 @@ 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.outputConstraintVersion); this.state = 'open'; clearTimeout(this.handshake.timer); this.handshake.resolve(this.caps); @@ -228,5 +289,6 @@ class PrivateConversation extends PrivateCompute { } } -module.exports = { PrivateConversation, validateConversation, validateResult, expectedLimits, requestLimit, capabilities, - check, keys, text, identifier, object, fail }; +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/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-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/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']); +}); diff --git a/tests/opencode-runtime.test.cjs b/tests/opencode-runtime.test.cjs index 6c63660..185063c 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'}}); @@ -26,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 c7bf932..d705cd6 100644 --- a/tests/opencode-session.test.cjs +++ b/tests/opencode-session.test.cjs @@ -19,8 +19,10 @@ 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']}, - provider: async value => { binding.provider = value.model; return provider; }, + {...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; + 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) { @@ -47,9 +49,28 @@ 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); + 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}}); + 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; } @@ -85,10 +106,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 +146,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/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/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..b10ef82 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'] }; @@ -85,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' }, 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/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 421eeee..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,25 +29,37 @@ 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. - 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: syntheticResult(output, conversation)}); + } } 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,18 +79,20 @@ 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'; 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, @@ -113,7 +135,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'))), @@ -128,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/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 ba82f81..460f827 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 @@ -129,6 +216,50 @@ 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), 3) + for child in reversed(sorted(native_blocks, key=lambda item: item.lineno)): + lines = source.splitlines(keepends=True) + # 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 = ( + (" 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", ''), + ("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"), + (",\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'", ''), + (" 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) + 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) @@ -174,12 +305,27 @@ 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', '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..ad35351 --- /dev/null +++ b/tests/test_opencode_native_cpu.py @@ -0,0 +1,1287 @@ +# 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 +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 Mock, 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, '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'): + 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_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)', + "'--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 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(), + '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)): + 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) | {'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)) + 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') + + @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'}) + 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 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' + 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()) + 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, + 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] + 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() + gguf = artifact(backend / 'model.gguf', b'inert gguf bytes, never loaded') + 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), + '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, + 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): + 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) + 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']) + 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')}, + {'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) + + 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) + # 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) + 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: + 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_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) + 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]) + 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_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': []}, + {'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], 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'): + 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']}, + {'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) + 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() + + @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() 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() 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() 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() 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 diff --git a/tests/test_smoke_opencode_inference.py b/tests/test_smoke_opencode_inference.py index e31cacc..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,6 +20,97 @@ 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('), 2) + final = guest.split(' finally:', 1)[1] + 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', + 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, @@ -47,7 +139,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='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')) @@ -57,7 +149,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, '6a517b576baa17e7329661ee0476d1848081d114') + 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)