Repository navigation
Conversation
|
Note for reviewers: on #2420 the decision is to take the default from the target's model-specific configuration, not from a gateway setting. I will rework this PR once the question in #2420 (comment) (how the gateway obtains the target limit) is settled, so please do not merge it in its current form. |
hidb4ai
left a comment
There was a problem hiding this comment.
Reviewed 84641150e5706d6b6556041fe38ebb3bb0b491fc: one Chat output-limit compatibility issue below.
| if request.output_limits.max_output_tokens.is_none() { | ||
| request.output_limits.max_output_tokens = default; |
There was a problem hiding this comment.
[P2] Use a target-compatible Chat token-limit field for defaults
With PC_GATEWAY_DEFAULT_MAX_OUTPUT_TOKENS=32000, a Chat request such as {"model":"o3","messages":[{"role":"user","content":"hi"}]} leaves token_limit_field as None, so the renderer emits max_tokens:32000. I confirmed those outgoing bytes with the actual gateway process and a recording upstream (Core/upstream were scripted, not live OpenAI). OpenAI's Chat contract excludes max_tokens for o-series models; they need max_completion_tokens, which the existing max_completion_tokens_field_name_preserved test already covers for explicit client limits. The new default therefore cannot unblock enforcing o-series Chat requests and makes otherwise valid omitted-limit requests incompatible when enabled. Select a supported wire field when supplying the default, preserve explicit client limits, and cover the omitted-limit o-series case.
There was a problem hiding this comment.
Thanks, agreed. When the gateway supplies the limit, Chat will use max_completion_tokens, which current OpenAI Chat models accept, including o-series, where max_tokens is rejected. Explicit client fields stay as they are, and the omitted-limit o-series case will be covered. This goes into the rework that takes the default from the target configuration, as decided on #2420.
There was a problem hiding this comment.
Rechecked the PR after your reply: the head is still 84641150e5706d6b6556041fe38ebb3bb0b491fc, so the emitted max_tokens issue remains in the code. The proposed max_completion_tokens change addresses this finding; please include the omitted-limit o-series regression case in the target-configuration rework. Approval remains pending the implementation and validation.
An enforcing Server requires every generation request to carry an output limit, and Codex 0.145 never sends max_output_tokens, so every unmodified Codex request was withheld with output_limit_required. PC_GATEWAY_DEFAULT_MAX_OUTPUT_TOKENS adds a limit to Chat and Responses requests that set none, before the check; a client's own limit is kept. The value is validated at startup. Refs oceanbase#2420. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on in Core Core's capabilities report each target's configured max_output_tokens in context_protection.output_limits. The gateway reads its own target's value at startup and fills it into Chat and Responses requests that set no limit, before the check; Chat uses max_completion_tokens, which o-series models accept. This replaces the gateway-side PC_GATEWAY_DEFAULT_MAX_OUTPUT_TOKENS setting, so the limit is configured only on the Server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For an openai-responses target that sets admit_encrypted_items, before_release admits encrypted reasoning in the response and, when the request names a conversation, returns a receipt sealed with the existing binding codec: the digests of those values and of the response's function-call arguments, bound to the principal, Scope, target and conversation for one hour. before_send admits an encrypted reasoning item or agent_message part only when a presented receipt for its conversation, or the parent conversation that handed a subagent its task, contains the value's digest; otherwise it withholds with encrypted-origin-unverified. Other encrypted positions stay opaque. The gateway keeps receipts per conversation and presents them with the conversation's later requests and with its subagents' requests. Refs oceanbase#2420. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…utput limits Records sqlite-atomic-829eca9a-v1 for Server source 829eca9: the wheel built from that commit (685 runtime files identical to the source) passed all four real_server.py modes with the Rust 1.95.0 native probe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8464115 to
82cde71
Compare
frf12
left a comment
There was a problem hiding this comment.
Reviewed 82cde710d9244d8c16d349fa23e3bce88eef8b38. The target-derived output limit and Chat field fix look good. Three remaining receipt-handling cases are described inline. @jasper-yl could you share your proposed handling for these cases?
| .receipts | ||
| .retain(|(_, expires)| *expires > now_ms); | ||
| if conversation.receipts.len() >= MAX_RECEIPTS { | ||
| conversation.receipts.pop_front(); |
There was a problem hiding this comment.
[P2] Active conversations lose required receipts after 32 responses
When each response adds a distinct reasoning ciphertext and subsequent requests retain the history, the 33rd response evicts the first receipt. Request 34 still carries that ciphertext and is withheld with encrypted-origin-unverified, even though no receipt has expired. A child fails on request 33: its own 32 receipts fill for_request's limit, so truncate(32) drops the parent's still-cached task receipt while the initial encrypted agent_message remains in its history. I reproduced both Core decisions at a fixed time using this FIFO/selection logic and the actual Core issuer/verifier; this was not a Rust gateway end-to-end run. Normal continuation therefore stops within the advertised one-hour validity window.
| return None, None | ||
| receipt = _issue_receipt( | ||
| request, | ||
| (*encrypted, *released_argument_strings(parsed)), |
There was a problem hiding this comment.
[P2] Ordinary tool arguments can suppress the entire receipt
released_argument_strings includes every string argument, including ordinary unencrypted values. With a declared string-array tool, a response containing 64 distinct ordinary argument strings plus one encrypted reasoning item produces 65 digests. _issue_receipt then returns None, although before_release returns allow. The next request replaying that same reasoning is withheld with encrypted-origin-unverified. I reproduced this through the public /v1/context/check route with a simulated provider response. A normal tool payload can therefore discard the provenance of the single encrypted item and prevent the conversation from continuing.
| for part in content: | ||
| payload = part.get("encrypted_content") if isinstance(part, dict) else None | ||
| if isinstance(part, dict) and part.get("type") == "encrypted_content" and isinstance(payload, str): | ||
| found.append(payload) |
There was a problem hiding this comment.
[P2] Admitting an encrypted part also hides its extra opaque fields
This branch removes the entire content part from the unsupported-content inspection. With a valid task receipt, an agent_message part containing type: encrypted_content, the receipted encrypted_content, and an extra image_url passes the public check with allow and coverage.complete=true. An extra nested content containing an input_file passes too. The equivalent standalone image is withheld with content_unsupported. Thus the exception also bypasses inspection of unsupported fields beyond the receipted ciphertext. These are Core HTTP-route reproductions with simulated inputs, not evidence that a real provider accepts the extended shape; the separate forbidden-plaintext check still blocks its configured values.
|
@frf12 Thanks, all three are real. Proposed handling: 1. Receipts evicted from an active conversation (
2. Ordinary argument strings void the receipt ( 3. An admitted part hides its other fields (
I'll add regression tests for your three reproductions through the public check route: the 34th request and a child's 33rd request, 64 ordinary argument strings plus one reasoning item, and the extended part with If you agree, I'll implement this on #2429. |
|
@jasper-yl Thanks for laying this out. Consolidating at A few suggestions for combining the approaches:
One detail to keep in mind when replacing stored receipts: consolidating a child request should leave the parent's receipts available to the parent, and a check finishing later should preserve any newer receipts added since its snapshot. This may be straightforward within the existing store. Does this fit with your approach? Please combine whichever parts are useful and let us know if you see a simpler way or a tradeoff we have missed. |
… declared and exact values Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records sqlite-atomic-64b5f85a-v1 for Server source 64b5f85: the wheel built from that commit (685 runtime files identical to the source) passed all four real_server.py modes with the Rust 1.95.0 native probe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@frf12 Thanks — all four points are in, pushed as
Your three reproductions are regression tests in |
frf12
left a comment
There was a problem hiding this comment.
Reviewed 6e9457216e0e1d1b0f716f87029043800c536f5e. Thanks for the update. I found one remaining case in the new consolidation flow, described inline. @jasper-yl could you take a look and share how you would handle it?
| declared = declared_encrypted_fields(parsed) | ||
| if declared is None: | ||
| return _RECEIPT_CAPACITY, None, () | ||
| digests = {value_digest(value) for value in encrypted} |
There was a problem hiding this comment.
[P2] Parent consolidation drops a pending child's task proof
A parent response containing reasoning ciphertext R and an encrypted spawn_agent.message T receives a release receipt covering both. If the parent continues before the child's first check, T is still in its function_call.arguments history, but this consolidation collects only the admitted reasoning/agent-message values, so the new receipt covers R alone. ReceiptStore::store then removes the presented original receipt (receipt_store.rs:104). The child's first request carrying T is consequently withheld with encrypted-origin-unverified, despite the original proof being unexpired. This requires only one parent continuation, not the receipt-count limit. I reproduced the sequence through the public /v1/context/check route with simulated provider payloads: the child is withheld with the consolidated receipt and allowed with the original release receipt. The Rust replacement path was inspected, not run end to end. A valid parent/child interleaving should retain the proof needed for the pending handoff.
There was a problem hiding this comment.
@frf12 Good catch, and thanks for reproducing it through the public route. Fixed in 0e530365b (plus the qualification record 0e3501529):
Approach. The send-time receipt now re-covers the values the request itself replays. before_send scans the request's replayed function_call items with the same declared-encrypted-field matching the release path uses, and adds those values' digests to the consolidated receipt — but only the digests the presented receipts already prove. So:
- a parent that keeps talking extends its consolidated receipt to the task ciphertext still sitting in its
function_call.argumentshistory, and the child's first request stays admissible after the replacement; - consolidation still mints no new proof: a replayed value without a presented, unexpired receipt stays unreceipted, exactly as before;
- the union still goes through the explicit capacity check, so a history too large to re-cover withholds with
encrypted-receipt-capacity-exceededinstead of silently dropping the handover's proof.
Verification. The regression test reproduces your sequence: parent release receipts R+T, the parent continues (presenting only that receipt, with the call in its history), and the child's first request presenting only the consolidated receipt is allowed — it was withheld with encrypted-origin-unverified before the fix. Your reading of ReceiptStore::store matches ours; the replacement semantics there are unchanged, Core just no longer strands the value. make check passes and the desktop contract is re-qualified against the new commit.
…ipts A parent that keeps talking after spawning a child replaces the receipts it presents at before_send. The consolidated receipt now also re-covers the encrypted argument values its replayed function calls carry when the presented receipts prove them, so the pending subagent handover keeps its proof. Values without a presented proof stay unreceipted and the capacity limit still withholds explicitly. Co-Authored-By: Claude Code <noreply@anthropic.com>
Records sqlite-atomic-0e530365-v1 for Server source 0e53036: the wheel built from that commit (685 runtime files identical to the source) passed all four real_server.py modes with the Rust 1.95.0 native probe. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
|
||
|
|
||
| @pytest.mark.anyio | ||
| async def test_only_declared_encrypted_arguments_are_receipted(): |
There was a problem hiding this comment.
[P2] 「只回执声明为加密的参数」缺少能检出回归的测试
encrypted_items.py 的 declared_encrypted_fields 只记录 "encrypted": true 的参数值,这是修复「普通参数值进入回执并取得准入证明」的关键;但本文件现有用例无法检出该过滤被移除。
变异证据(D:\test\github\review\oceanbase-powercontext-pr-2429\probes\,一次性副本,exact head 0e350152,未改动被审仓库):把
declared_encrypted_fields 中 isinstance(schema, dict) and schema.get("encrypted") is True 改成 isinstance(schema, dict)(即所有参数都算"声明加密"),本文件 18 个用例全部通过。
同一变异下 probes/probe_declared_filter.py 的公开路由行为发生变化:请求带 TOOLS(spawn_agent.message 声明加密、task_name 未声明),release 响应携带 SPAWN(task_name="helper"、message="gAAAA-task"),随后把 "helper" 当作 agent_message 的 encrypted_content 重放——未变异 head 得到 withhold ('encrypted-origin-unverified'),变异后得到 allow ()。
现有用例为什么覆盖不到:
- 本函数(
test_only_declared_encrypted_arguments_are_receipted)的请求只有[_user(...)],没有TOOLS,declared=()与"完全没有声明"等价,不检验声明过滤; test_ordinary_tool_arguments_do_not_displace_encrypted_values的普通值放在tags数组里,已被_argument_values的isinstance(argument, str)提前过滤。
影响:随后任何把"声明过滤"放宽的改动(例如为兼容某宿主改成"所有字符串参数都回执")都不会被 CI 拦住,普通参数值可以重新取得准入证明,而这正是上一轮评审要求修掉的缺陷形状。
建议:补一条用例——请求带 TOOLS,release 携带 SPAWN,再把 task_name 的值重放为 encrypted_content,断言 encrypted-origin-unverified;当前实现应通过该用例,上述变异应使其失败。
There was a problem hiding this comment.
@AlexStocks 谢谢,变异测试很有说服力。已按你的建议补上,见 2ba69243f:
- 新增
test_an_undeclared_argument_of_a_declared_tool_proves_nothing。父请求带上 Codex 的additional_tools声明(spawn_agent.message声明为加密,task_name未声明),release 返回SPAWN。随后:- 把
"helper"(task_name的值)作为子代理agent_message的encrypted_content重放,断言encrypted-origin-unverified; - 把真正的任务密文作为对照重放,断言放行。
- 把
- 我按你的变异做了验证:把
schema.get("encrypted") is True改成只判断isinstance(schema, dict),这条用例失败,其余用例不受影响;恢复后全部通过。
tests (3.14, unit) failed only on the runner: the handoff statement-count test passes on 3.14 locally, single and full suite, and the same stack is green as oceanbase#2429. No file changes. Co-Authored-By: Claude Code <noreply@anthropic.com>
tests (3.14, unit) failed only on the runner: the handoff statement-count test passes on 3.14 locally, single and full suite, and the same stack is green as oceanbase#2429. No file changes. Co-Authored-By: Claude Code <noreply@anthropic.com>
hidb4ai
left a comment
There was a problem hiding this comment.
Reviewed the current head. Two receipt-origin and continuation issues remain; details are inline.
| } | ||
| match format { | ||
| Format::Responses => { | ||
| header_id(headers, CODEX_PARENT_THREAD_HEADER).map(|id| format!("codex-thread:{id}")) |
There was a problem hiding this comment.
[P2] Verify the parent-to-child handoff before forwarding its receipts
A Responses client can set thread-id to an unrelated conversation and x-codex-parent-thread-id to any known conversation in the same Scope. This code forwards that parent's receipts, and Core accepts every matching digest, including the parent's reasoning ciphertext, without checking that this child was spawned by that parent or that the ciphertext was handed over as its task. With the actual Core issuer/verifier and simulated payloads, an unrelated child's replay of the parent's reasoning is withheld without the claimed parent and allowed with it. This violates the verified parent-to-child restriction in #2420; the same principal/Scope/target checks do not establish that relationship. Please authenticate the handoff and restrict inherited proof to the values it authorized. This reproduction exercised Core directly; the gateway header-to-receipt path was inspected, not run end to end.
There was a problem hiding this comment.
@hidb4ai Thanks, confirmed. A named parent's receipts were accepted for any matching digest. Fixed in 2ba69243f by restricting what a claimed parent can lend:
- Handover values. Receipts now mark handover values separately. These are the function-call argument values the checked request's tool definitions declare
"encrypted": true, which for Codex isspawn_agent.message,send_message.messageandfollowup_task.message. - What a parent's receipt can prove. A receipt issued to the request's
parent_conversation_idproves only those handover values, and only in anagent_messageencrypted_contentpart, which is where a subagent receives its task. It never proves the parent's reasoning, at the reasoning position or disguised as anagent_message. Your reproduction (an unrelated child replaying the parent's reasoning with the parent named) is now withheld withencrypted-origin-unverifiedeither way. - Regression test:
test_a_claimed_parent_lends_only_its_handover_valuescovers three cases: the parent's reasoning replayed as reasoning, the same value as anagent_message, and the task value replayed as reasoning.
What this does not establish. Core still cannot prove that the conversation naming a parent is the subagent that parent actually spawned. The child's thread ID is assigned by the client and reaches Core only in the parent's later spawn_agent output, which can arrive after the child's first request. So a conversation of the same principal, Scope and target that names the parent can receive that parent's declared task values, and nothing else.
Binding the handover to the child's identity would need the parent's tool result to precede the child's first request, which Codex does not guarantee. If you consider the remaining case blocking, I'd suggest binding each handover value to a single child conversation on first use. I can add that.
Verified end to end as well: an unmodified Codex 0.145 against the real OpenAI API, through the gateway and Server built from this commit in enforcing mode, completed the parent-plus-subagent run with no withheld check. The child's task was admitted as the parent's declared handover value.
| }; | ||
| conversation | ||
| .receipts | ||
| .retain(|r| r.expires_ms > now_ms && !replaced.contains(&r.id)); |
There was a problem hiding this comment.
[P2] Preserve unexpired proof needed by divergent requests
Consolidation deletes every presented receipt, but the replacement covers only ciphertexts and declared arguments in this particular request. Suppose an unexpired receipt covers reasoning values R and X, and a retry or concurrent branch submits an earlier history containing only R. Its send check returns a receipt for R, and this replacement removes the only proof of X. A subsequent valid request on the other branch that still replays R and X is then withheld with encrypted-origin-unverified, even though X was released and its original receipt has not expired. I reproduced the Core decisions with simulated responses and applied the replacement logic shown here; I did not run the Rust gateway end to end. Please retain proof needed by other in-flight or retried histories when consolidating.
There was a problem hiding this comment.
@hidb4ai Agreed, consolidation must not delete proof another history still needs. Fixed in 2ba69243f, on the Core side, so the gateway's replacement stays simple:
- Receipts carry proofs with their own expiry. A receipt groups its digests by expiry, rounded down to the minute so a carried proof never outlives its original receipt.
- Consolidation carries every unexpired proof. The
before_sendreceipt now carries every unexpired proof of the conversation's own presented receipts, each with its original expiry. It renews for one hour only the values this request carries, plus the proven handover values its replayed function calls carry. The parent's receipts are never folded into a child's. - Effect in your scenario. The receipt that replaces the presented ones proves X as well as R, so the branch replaying R and X still verifies. X still expires when its original proof would have, because an unused value is not renewed.
- Regression tests:
test_a_divergent_history_keeps_the_proof_another_branch_needscovers your sequence: R and X released, a retry replaying only R consolidates, and the branch replaying both is allowed with only the consolidated receipt.test_a_carried_proof_keeps_its_original_expirychecks that X is withheld after its original hour while the renewed R is still admitted.
- Capacity. The total still goes through the explicit 1,024-value check.
…ut_limits 85017ff added the Rust field without running export_ipc, so desktop-validation ipc:check reported drift. Regenerated with cargo run --example export_ipc. Co-Authored-By: Claude Code <noreply@anthropic.com>
…oof to handover values A consolidated receipt now carries every unexpired proof of the receipts it replaces, with its expiry, so a retried or divergent history keeps the proof another branch still needs; only the values a request uses are renewed. Receipts mark the declared encrypted arguments as handover values, and a subagent may use its parent's receipts only for those values as its agent_message task, never for the parent's reasoning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records sqlite-atomic-2ba69243-v1 for Server source 2ba6924: the wheel built from that commit (685 runtime files identical to the source) passed all four real_server.py modes with the Rust 1.95.0 native probe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1aa509b to
74ebb73
Compare
|
Two updates on #2429. 1. Corrections to my summary of
2. Branch history. I removed two empty "retrigger CI" commits from the branch and force-pushed. The |
Which issue or RFC does this PR close?
Closes #2420: both parts, the output-limit default and encrypted-content admission, as decided there for the combined deployment. Retry handling is #2428 and is not included.
Rationale for this change
An unmodified Codex 0.145 could not complete a turn through an enforcing Server, for two reasons:
max_output_tokens, so every request was withheld withoutput_limit_required.store: falsewithinclude: ["reasoning.encrypted_content"], so every response carries encrypted reasoning that the next requests replay. A subagent receives its task as an encryptedagent_messagepart, which the parent's model returned insidespawn_agentarguments. Core withheld all of these ascontent_unsupported.What changes are included in this PR?
Output limit, from Core's own target configuration
context_protection.output_limits: each target's configuredmax_output_tokens.PC_GATEWAY_UPSTREAM_PROFILE) from capabilities at startup. It fills that value into Chat and Responses requests that set no limit, before injection and the check. On Chat it usesmax_completion_tokens, which o-series models accept. A client's own limit and field are kept, and the final request is checked as before.PC_GATEWAY_DEFAULT_MAX_OUTPUT_TOKENSis gone.Encrypted items, admitted only with receipts (existing binding codec, no persisted state)
Opt-in.
UpstreamProfile.admit_encrypted_items(reasoning,agent_message), foropenai-responsestargets only. Empty by default, so nothing changes without it.Inspection. For such a target, encrypted values at exactly these positions are set aside before the opaque-content check:
encrypted_contentof a root-levelreasoningitem (its other fields stay inspected);agent_messagecontent part whose only fields aretype: "encrypted_content"andencrypted_content.A part with any other field, and every other encrypted value or opaque type, still withholds. Only the context check enables this; Core's own protected inference transport is unchanged.
Receipts. Sealed with the binding codec under their own purpose, bound to the principal, Scope, target and conversation. Values are 16-byte digests grouped by expiry (rounded down to the minute), and a value is marked as a handover value when it is a declared encrypted argument. One receipt holds at most 1,024 values (about 30 KB sealed). A check whose values do not fit withholds with
encrypted-receipt-capacity-exceeded, so no later request silently loses its proof.before_release. When the request names a conversation, Core returns a receipt valid for one hour."encrypted": true. Codex declaresspawn_agent.messagethis way, in itsadditional_toolsnamespace. The send binding records the declared fields for the release.before_send.conversation_id.parent_conversation_idproves only handover values, and only in anagent_messagepart. A subagent can receive its task, never the parent's reasoning.encrypted-origin-unverified. Without a conversation nothing is receipted or inherited.Consolidation. An allowed
before_sendwith a conversation returns one receipt that replaces the conversation's receipts the request presented.Gateway.
ReceiptStorekeeps receipts per conversation.x-codex-parent-thread-idon the Responses route, the Claude Code parent agent (or the main session for a top-level subagent), and the OpenCode parent session.Admission proves where a value came from, not what it contains: Core has not inspected the plaintext. The docs say so.
Contract and docs
openapi/powercontext.yaml:conversation_id,parent_conversation_idandencrypted_receiptsonbefore_send;conversation_idonbefore_release;receiptin the result, atbefore_sendandbefore_release;output_limitsin capabilities.pnpm --dir desktop generate) and the IPC bindings (export_ipc) are regenerated.sqlite-atomic-2ba69243-v1incompatibility.jsonanddesktop/VALIDATION.md(details below).inference-protection.mdanddeploy-server.mdin English and Chinese, andgateway/README.md.Are there any user-facing changes?
max_output_tokensinstead of being withheld.encrypted-receipt-capacity-exceeded.null.How was this change tested?
tests/builtin/runtime/test_context_check_encrypted.py: 22 tests with Codex request and response shapes (including Codex'sadditional_toolsdeclaration):content_unsupported;nullvalidate.agent_message.encrypted: truefilter);image_urlor nestedcontentis inspected whole;output_limits.make checkandmake contract-testpass.release_receipts_are_presented_for_the_conversation_and_its_subagents;ReceiptStorebounds and expiry, consolidation that keeps receipts added after the snapshot and the parent's receipts, and an active conversation's bounded receipt count;max_completion_tokensfor an o-series request with no limit, with a client's ownmax_tokenskept.gpt-5.6-sol), through a Server and a gateway built from this branch, enforcing mode, with no shim. The target hasmax_output_tokens: 32000andadmit_encrypted_items: ["reasoning", "agent_message"]. The parent was asked to start one subagent that replies "pong".null, so every check failed with 422. It is fixed in this PR and covered by a test.2ba69243f(consolidated receipts, declared handover values, exact admission), it completed the same way, with no withheld check. A recording pass-through between the gateway and OpenAI showed:max_output_tokens: 32000, filled from Core;agent_messagepart, admitted with the parent's receipt as a declared handover value (spawn_agent.messagein Codex'scollaborationnamespace).uv build --wheelfrom the clean checkout of2ba69243f. Its 685 runtime files are identical to the commit (manifest digest63b28e11…).desktop/tests/real_server.pypassed anonymous loopback, static Bearer, HTTPS with an explicit CA and base path, and injected-provider/enforced-access, with the Rust 1.95.0server_probe.desktop/VALIDATION.md#server-2ba69243-protocol-qualification.AI usage statement
Built with Claude Code (Claude Opus 5.5), which wrote the change, the tests, the docs and the qualification record, and ran the checks above.
🤖 Generated with Claude Code