Skip to content

server: "execution_witness": true binds a reply to the witness at admission and at its end, with its prompt, resolved request and tokens - #2958

Open
kotevcode wants to merge 12 commits into
ashhart:mainfrom
kotevcode:execution-witness-reply
Open

kotevcode wants to merge 12 commits into
ashhart:mainfrom
kotevcode:execution-witness-reply

Conversation

@kotevcode

@kotevcode kotevcode commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

With --execution-witness, a chat completion or completion that sends "execution_witness": true gets a top-level execution_witness object: on the whole reply, or on a stream's finish chunk. It is known only when the witness was known just before the engine took the request and again once the reply ended, with the same epoch and witness. In that case it carries a binding: a SHA-256 over the witness, the prompt's token ids, the resolved request and the reply's token ids, plus the finish reason. Otherwise it is unknown with a reason. Without the field, replies are byte-identical; without the flag, true answers 400.

Checks on this head

  • Head commit: abeba05
  • Lean checker and python3 tools/pr_check.py --base upstream/main --pr 2958: lean 345 and 345 on the base and this head, 0 new; 0 errors, 0 warnings.
  • Tests run, with the command and pass, fail and skip counts (macOS 26.6, M5 Max):
    • zig build test-execution-witness-host: 43 passed, 0 failed, 0 skipped; 9 new in witness_reply_test.zig. Flag off with true gives 400 (a non-boolean too) and the engine is never called; flag on with the field absent or false gives the same bytes as flag off. A known binding has the read route's witness, instance and path, prompt_sha256 of the ids, token_sha256 equal to tensorfold.token_sha256, and request_sha256 and binding recomputed from the reply; another max_tokens changes the request and binding but not the prompt. Unsupported path, --slide and files_changed give unknown with every digest. An engine whose epoch moves inside submit gives epoch_changed (admitted 1, end 2). A stream with include_usage binds once, on its finish chunk, equal to the whole reply. A sampled reply names the seed it drew, or the one it was given, and a given seed binds the same twice. 8 concurrent replies each bind their own prompt to the one witness. An inference-only key sending true gets 403 on completions and chat with the engine never called, and false from it runs; [inference,read], admin and full keys bind on both. Responses, the Messages route and token counting answer 400 for true and 200 without it.
    • Before the change: with openai.zig restored to main's, the first 7 new tests fail (1 failed, 6 crashed on the missing object); without the scope check the inference-only key gets 200, and without the translators' refusal the three routes answer 200.
    • zig build test-server-cpu: 1347 passed (and 5 in a second binary), 0 failed, 7 skipped, on today's main. zig build test-key-scopes-host: 33 passed, 1 new for witnessDenial. zig build test-model-idle-host: 70 passed. zig build test-cuda-host (on 29294bf; the review changes touch no CUDA file): 383 of 383 steps, 6268 passed, 0 failed, 38 skipped.
    • zig fmt --check build.zig zig and git diff --check pass.
  • CI: rebased onto today's main so it can run.
  • Not run, and why: chat-route bindings over the test socket server. The fixture's byte tokenizer has no template, so the tests use /v1/completions; both routes go through the same generate and extras. Metal steps: no Xcode Metal toolchain here.

Receipt

  • Main against branch: below, on an A100, with the flag and field off and on. Raw results, the driver and the analysis scripts: https://gist.github.com/kotevcode/5b4d26cd284c1f04878862f4c1611b93
  • Boxes it reaches: one A100-SXM4-40GB (sm_80).
  • New switch? A request field, execution_witness, default false; no new serve flag or profile row.
  • Environment: base eab305d; branch e5eede8e5, which is native: Qwen3.5-2B on CUDA attests the execution witness's first path, every weight byte read once the server is ready #2956's head with this PR's commits before the review on top, plus one test-only change for the stack (the reply tests call start(srv), native: Qwen3.5-2B on CUDA attests the execution witness's first path, every weight byte read once the server is ready #2956's signature); Zig 0.17.0; nvcc 13.0.88, driver 580.178.04; Ubuntu 24.04, 12 vCPU, 83 GiB; zig build -Dgpu=sm_80; mlx-community/Qwen3.5-2B-MLX-4bit@93760be4f1f69842a46bc13dbdc0f19e291392a3, --context 8192 --no-thinking --api-key, a fresh process per start. Both binaries rebuilt byte-identical on a second node.
  • Paths and platforms the change reaches, which I ran and which I could not: chat completions and completions on the native Qwen3.5 CUDA path. Ran: CUDA sm_80, 2B. Not run: other SMs, HIP, Metal, Windows.
  • How the field was sent: the repo's tools have no extra-body option, so they ran unchanged through a small wrapper that adds "execution_witness": true for C and records each object; A and B go through the same wrapper without it. C's tool replies gave 244 objects, all known, each token_sha256 equal to the reply's tensorfold.token_sha256.
  • Main against this branch, same machine and checkpoint: A main, B branch, C branch with --execution-witness and the field on every request, measured after known; medians, alternating ABC, BCA, CAB.
2B A B C
Start to /health 200, s (n=8) 5.073 5.119 5.037
Cold first token, 7,680 tokens, ttft_s (n=8) 0.362 0.366 0.365
Decode, 1 stream, code / chat, tok/s 835.5 / 414.3 834.9 / 414.6 834.7 / 414.7
Decode, 8 streams aggregate, code / chat, tok/s 3285.7 / 2052.2 3287.8 / 2053.9 3281.5 / 2052.8
Decode, 8 streams per stream, code / chat, tok/s 420.7 / 276.7 421.5 / 276.5 421.1 / 275.7

C is 0.13% below A at 8 streams (code aggregate) and 0.36% (chat per stream), inside the run-to-run spread of #2956's run.

  • Exactness: bench_concurrent.py --alone --serial at 1 and 8 streams passed in every cell for all three (9/9 alone at 1, 72/72 at 8, 3/3 serial, 0 unequal). Each prompt gave one token_sha256 across A, B and C, the same as native: Qwen3.5-2B on CUDA attests the execution witness's first path, every weight byte read once the server is ready #2956's run: code 579bcb3b…, chat 12faa747…, the cold 7,680-token prompt 924e91cc….
  • Binding, on C: 70 objects, each with request_sha256 and binding recomputed from the reply, token_sha256 equal to tensorfold.token_sha256, and witness, epoch, instance and path equal to GET /v1/execution/witness; 0 failures.
    • "yes" gives 400; false or absent gives no object and the same tokens; B with true gives 400; main ignores the field.
    • Repeated: three replies and one stream give one binding; the stream carries it once, on the finish chunk.
    • Fresh and resumed: on CUDA this model keeps prompt states only with --prompt-cache-gib, so that check adds --prompt-cache-gib 4. Turn 2 resumed (4,096 of 5,786 tokens cached) and fresh give the same token, prompt and request digests; the bindings differ only through the witness (a new instance). The flag doesn't change content or components.engine.
    • Drafted and "draft": false: same tokens and prompt; the requests differ only in drafts.
    • Solo and concurrent: 8 prompts at once, each solo, then 8 at once again: token, prompt, request and binding equal for all 8.
    • Sampled at 0.7 with no seed: request.sampling.seed names the drawn seed; sending it back gives the same tokens, request and binding (twice each).
    • Idle reload (--idle-unload 30): known at epoch 1, then parked; the request that wakes it reads unknown, pending, admitted at epoch 2, and so do the requests until the re-hash ends 21.4 s later; then known at epoch 2. Same tokens in all 14 replies; none read epoch_changed or witness_changed.
  • Prompt speed before and after: tools/bench_cache.py --require-cold, 7,680-token prompt: above, every run cold. Not run: 2k, 32k and 64k prompts.
  • Licence of any new weights or drafter: n/a.

Checklist

  • The title and description match the head commit, including the receipt and the stack.

  • New tests fail before the change, pass after it, and skip cleanly without their dependency.

  • Comments and docstrings are one line. No measurements or history in the source.

  • No personal data, machine names, internal hosts or local paths in the diff or its commits. No AI attribution lines.

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.

  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.

  • No precision traded for speed. If the bits change, the description says which sums change and why.

  • Decode and prompt processing are no slower than main at one stream and the widest admitted (8).

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

@kotevcode thank you, this is a careful third part. The parts we care most about are right. The field is off unless asked, and a reply without it is byte for byte the same (zig/src/server/witness_reply.zig:16-21, chat.zig:253). The binding is known only when both reads agree on epoch and witness, and the epoch only moves forward, so a reload anywhere in between reads epoch_changed instead of a binding that can't be trusted. The tagged, length-prefixed request_sha256, recomputed in the test from the parsed reply, is a nice touch.

A few things before it leaves draft:

  1. Scope: the read route refuses an inference-only key (witness_http_test.zig:85-87), but "execution_witness": true hands that key the witness, path, instance and epoch. Could asked refuse the field without the read scope, with a test that uses the inference-only key? If you see a reason these four should be open to any inference caller, say so here and we'll weigh it.
  2. The ask from server: --execution-witness serves a read-scoped snapshot of what runs, or why it is unknown #2901: TENSORFOLD_BENCHMARK_TOKEN is still hashed into the environment component, because witness_parts.secret (zig/src/server/witness_parts.zig:159-162) catches only _API_KEY, _SECRET and _PASSWORD. Please add it, by exact name or a _TOKEN suffix, with a test. It can land on server: --execution-witness serves a read-scoped snapshot of what runs, or why it is unknown #2901 and ride up the stack.
  3. Responses and Messages: since those routes rebuild their bodies, a client sending "execution_witness": true there gets no binding and no error. Please refuse the field by name on those routes until they carry it.
  4. The A100 run you described, with native: Qwen3.5-2B on CUDA attests the execution witness's first path, every weight byte read once the server is ready #2956 stacked: flag and field off and on, at 1 and 8 streams, fresh and resumed, solo and concurrent, with token hashes. That's the receipt this needs.
  • Synthena (Ash's Agent)

kotevcode added a commit to kotevcode/TensorFold that referenced this pull request Nov 12, 2026
@kotevcode
kotevcode force-pushed the execution-witness-reply branch from 29294bf to d8a0af5 Compare November 12, 2026 05:41
@kotevcode

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All four points are addressed, in d8a0af5 on top of #2901's bd15bd6:

  1. Scope. "execution_witness": true from a key that has inference but not read now gets the same 403 the read route gives: permission_error, code insufficient_scope, param: "execution_witness", and the connection is closed. The check is auth_scope.witnessDenial, next to responseDenial, and it runs in openai.post right after the body is read, before plan. It sits there rather than in asked because plan doesn't see the key, and its other callers (imprint, the Responses warmup) never return a binding. Admin keys, scope-less (full) keys and [inference,read] keys keep the field. "execution_witness": false from an inference-only key still runs normally. New tests: an HTTP test where the inference-only key is refused on both /v1/completions and /v1/chat/completions before the engine is called (0 engine calls), and [inference,read], [admin] and plain keys each get a known binding on both routes; plus a unit test of witnessDenial in auth_scope_test.zig.

  2. Secret filter. witness_parts.secret now also excludes names ending in _TOKEN. I checked the repo: TENSORFOLD_BENCHMARK_TOKEN is the only TF_/TENSORFOLD_ name with that suffix. Settings like TENSORFOLD_PREFILL_TOKENS, TF_PAGE_TOKENS and TF_DSV41_POOL_TOKENS end in _TOKENS, so they are still hashed, and the existing test still checks that TENSORFOLD_PREFILL_TOKENS moves the component. The environment test now sets TENSORFOLD_BENCHMARK_TOKEN and asserts that the component doesn't change. docs/api.md lists the suffix. This one is a new commit on server: --execution-witness serves a read-scoped snapshot of what runs, or why it is unknown #2901, and server: "execution_witness": true binds a reply to the witness at admission and at its end, with its prompt, resolved request and tokens #2958 is restacked on it.

  3. Responses and Messages. Both translators now refuse a true execution_witness by name with 400: "execution_witness is supported on /v1/chat/completions and /v1/completions only". That covers /v1/responses (HTTP and socket, since both go through responses_translate), /v1/messages and /v1/messages/count_tokens. The test checks that each route answers 400 with that message and the engine isn't called, and that the same bodies without the field answer 200. Docs note the refusal under Reply binding, the Responses refusal list and the Messages section.

  4. A100 receipt. It's in the PR description (https://gist.github.com/kotevcode/5b4d26cd284c1f04878862f4c1611b93). The binding run is no slower than main at 1 and 8 streams. 70 bindings were recomputed with 0 failures. Fresh/resumed, drafted/draft: false, solo/concurrent, sampled seed and idle reload all hold.

Each new test fails without its change. Witness, key-scope, server-cpu and model-idle suites pass, and pr_check reports 0 new lean findings on both PRs.

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @kotevcode, all four are in d8a0af5, and they hold up:

  • auth_scope.witnessDenial refuses true from an inference-only key before plan or the engine runs. Admin, full and [inference,read] keys keep it, and false from an inference-only key still runs.
  • The _TOKEN filter came in on server: --execution-witness serves a read-scoped snapshot of what runs, or why it is unknown #2901, and it's ready to land there.
  • Responses (over HTTP and the WebSocket) and Messages, including count_tokens, refuse a true field by name. Without the field they answer 200.
  • The A100 receipt covers what we asked for: flag and field off and on, 1 and 8 streams, fresh and resumed, drafted and "draft": false, solo and concurrent, a sampled seed sent back, and an idle reload. All 70 bindings recompute from the reply, with no failures.

Merged onto today's main here, test-execution-witness-host and test-key-scopes-host pass. I also checked request against engine_api.Request. Every field the engine reads to choose a text reply's tokens is in it. The ones left out are scheduling or prefill hints, tools_json, which no engine reads, and images, which can't reach an attested path while --vision reads feature_excluded. If a later path drops that exclusion, the images will need to go into the binding too.

Two small things, then it's ready:

  1. zig/src/server/witness_parts.zig:159-164: two more credential names still reach components.environment. TF_RUNTIME_SIGNING_KEY holds the runtime feed's signing key (.github/workflows/runtime-feed.yml:101). TF_STAGE_SECRET_FILE points at the pipeline stages' shared secret (zig/src/fabric/stage/config.zig:35). Both fit the function's own rule, "credentials or point at them", the same way TENSORFOLD_TOOL_ID_KEY_FILE does. Please also leave out names ending in _SIGNING_KEY, _SECRET_FILE and _KEY_FILE. No other setting in the repo ends that way, so a name like TF_SLIDE_POCKET_KEY is still hashed. Add a line for them to the environment test and to the list in docs/api.md. server: --execution-witness serves a read-scoped snapshot of what runs, or why it is unknown #2901 lands first, so this can be a commit here.
  2. zig/src/server/witness_reply.zig:210: missing() can answer reason: "unbound", but the reasons listed in docs/api.md:1132 don't include it. Please add it there and say when a client can see it.

After that, please take it out of draft. CI hasn't run on d8a0af5 because CHANGELOG.md conflicts with main. A merge or rebase onto today's main lets it run, and the changelog line itself gets placed when we land. This PR doesn't need #2956, so it can follow #2901 straight away.

  • Synthena (Ash's Agent)

@kotevcode

Copy link
Copy Markdown
Contributor Author

Thanks, both done, and the branch is rebased onto today's main through #2896's new head (a7c119d, 15a9a32). The old #2896 commits are gone, and the moved engine_admission.zig is byte-identical to 15a9a32's.

  • 3cfc1c2: secret() now leaves out names ending in _SIGNING_KEY, _SECRET_FILE and _KEY_FILE. I grepped every TF_*/TENSORFOLD_* name in the tree, and only TF_RUNTIME_SIGNING_KEY, TF_STAGE_SECRET_FILE and TENSORFOLD_TOOL_ID_KEY_FILE end that way. That made both exact names redundant (TENSORFOLD_API_KEY already ends in _API_KEY), so secret() is now a single list of endings. The environment test pins the three new names as left out and TF_SLIDE_POCKET_KEY as still hashed. The components.environment row in docs/api.md lists the endings and names those examples.
  • abeba05: unbound is now in the reply-binding reasons, and the docs say when it appears. missing() answers unbound only when the reply carries no binding while the witness reads known. No route gets there today. A reply misses its binding only on the --compact-at path (compact.run/compact.prepare) or a Cortheon turn (srv.cortheon, set only by --cortheon), and both flags are in excluded(), so the witness reads feature_excluded first. Every other path goes through chat.generate, which binds whenever "execution_witness": true passed asked(). unbound is defensive: it marks a future reply path that skips the binding by mistake.

The CHANGELOG lines for #2958 and #2901 sit at the top of Unreleased, above #2896's and main's. test-execution-witness-host 43/43, test-key-scopes-host 33/33, test-server-cpu 1347 pass and 7 skip, test-model-idle-host 70/70; fmt, git diff --check and pr_check are clean.

@kotevcode
kotevcode marked this pull request as ready for review November 12, 2026 16:48
@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @kotevcode, both are in and they hold up.

  • 3cfc1c218: secret() (zig/src/server/witness_parts.zig:159) is now one list of endings. TF_RUNTIME_SIGNING_KEY, TF_STAGE_SECRET_FILE and TENSORFOLD_TOOL_ID_KEY_FILE stay out of components.environment, and TF_SLIDE_POCKET_KEY is still hashed. The environment test pins both sides, and docs/api.md lists the endings with those examples.
  • abeba0530: missing() (zig/src/server/witness_reply.zig:204) answers unbound only when the snapshot reads known. docs/api.md:1132 lists it, and :1146-1148 says when it can appear: only on a reply path that skipped the binding by mistake, since today every path that binds nothing runs under a flag the witness excludes.

CI is green on abeba0530, and the branch merges cleanly with today's main. The #2901 commits it carries make the same change as #2901's own head. Against #2903, which lands ahead of it, the only conflict is the CHANGELOG.md Unreleased lines, which we sort out when landing.

No more asks. This is ready, and we'll land it right after #2896, #2903 and #2901. Thanks for seeing all three parts through so carefully.

  • Synthena (Ash's Agent)

ashhart pushed a commit to kotevcode/TensorFold that referenced this pull request Nov 13, 2026
@ashhart
ashhart force-pushed the execution-witness-reply branch from abeba05 to b00760d Compare November 13, 2026 15:29
@ashhart

ashhart commented Nov 13, 2026

Copy link
Copy Markdown
Owner

@kotevcode main moved under this and it conflicted with main in docs/api.md zig/src/server/chat.zig zig/src/server/cli.zig (it builds on #2901, which was rebased first), so the landing train left it out. To save you a round trip we rebased it onto current main and pushed it to your branch: each conflict keeps main's change and yours, and your commits and authorship are otherwise unchanged. The host tests for its files, the serving-pin check and our privacy gate pass on the new head. It goes back in the landing queue once its CI is green. No other asks.

  • Synthena (Ash's Agent)

@ashhart
ashhart force-pushed the execution-witness-reply branch from b00760d to d88afbf Compare November 13, 2026 16:39
ashhart pushed a commit to kotevcode/TensorFold that referenced this pull request Nov 13, 2026
@ashhart
ashhart force-pushed the execution-witness-reply branch from d88afbf to cf9b21c Compare November 13, 2026 17:38

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants