Skip to content

feat(gateway): add Rust PowerContext Gateway executor - #2307

Merged
hidb4ai merged 38 commits into
oceanbase:masterfrom
ztelur:feat/rust-gateway
Nov 2, 2026
Merged

hidb4ai merged 38 commits into
oceanbase:masterfrom
ztelur:feat/rust-gateway

Conversation

@ztelur

@ztelur ztelur commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Which issue or RFC does this PR close?

Implements the Rust gateway side of the design in #2199 and consumes the external-executor contract from #2264.

Depends on #2264 (/v1/context/check two-phase contract and include_reuse). This PR adds a new gateway/ directory only and does not touch any existing module; it can be reviewed and merged after #2264 lands.

This PR adds one: a self-contained Rust workspace that acts as that external executor, verified end-to-end against a real PowerContext Server over UDS with real upstream providers.

What changes are included in this PR?

A new gateway/ directory (Rust workspace, 7 crates, ~3.7k lines, zero changes to existing Python code):

  • Two-phase gate executor: check(before_send) → send admitted bytes → check(before_release) → release. Opaque bindings thread the phases (request_binding must equal the send-phase binding); HTTP success alone is never approval; unknown never becomes allow.
  • Memory injection: per user message, calls prepare with include_reuse, injects the returned prepared context verbatim into the current user message, and declares the exact UTF-8 byte range in prepared_contexts for Core-side digest verification. Tail-only injection keeps the client-owned prefix byte-stable (KV-cache friendly, verified with DeepSeek cached_tokens).
  • Sedimentation: after a released turn, synchronously captures the transcript via capture_content_source with an idempotent source_id derived from the release binding. Blocked/degraded turns are never captured. The full loop (conversation → capture → background extraction → prepare recall in a new session) was verified end-to-end.
  • Deployment forms: combined (Rust owns the public TCP port; Core listens only on a private UDS; all non-/gateway routes reverse-proxied over UDS) or standalone (TCP). Pooled hand-rolled HTTP/1.1-over-UDS client with timeouts, stale-connection retry, and chunked/content-length framing regression tests.
  • OpenAI-compatible ingress (/gateway/v1/chat/completions and /gateway/v1/responses, same format in and out), streaming ingestion with strict buffering, upstream status/content-type fidelity.
  • Middleware chain (GatewayMiddleware trait) with resource limits (body precheck + concurrency semaphore) and request-id propagation aligned with HttpRequestIdMiddleware.

Contract findings from real integration (relevant to #2264 review):

  1. External executors have no usage-reporting endpoint — provider usage collected by a gateway cannot flow back to Core statistics.
  2. The public check carrier withholds streaming payloads (streaming_unsupported), so external executors are effectively non-streaming under strict mode.
  3. prepared_contexts and ModelJsonTextLocation.schema are required wire fields that are easy to miss (both cost us a 422 during integration).

Are there any user-facing changes?

No changes to existing APIs, storage, or behavior. This adds an optional component: a gateway/ Rust workspace, off by default, not wired into packaging or the Server. Existing users are unaffected.

How was this change tested?

  • cargo clippy --workspace --all-targets: zero warnings. cargo test: 29 tests — failure-semantics table (gate unavailable/withhold terminal/bounded retry/indeterminate no-resend/malformed never degrades/non-UTF-8 never released/blocked turns never sediment), renderer fidelity (no timestamps or request ids injected), prepared-context byte-range declaration, UDS connection-pool correctness over a real Unix socket.
  • Live integration tests against a real Server (tests/live_context_check.rs, skip gracefully when unreachable): real signed bindings, cross-scope binding rejection, send vs correlation deadline separation.
  • Manual end-to-end against a real Server over UDS with two real upstream providers (DeepSeek and GLM via an approved profile): per turn exactly check ×2, prepare ×1, capture ×1 in Server logs; forbidden content blocked with zero sends; memory injected in a fresh session answered from previously sedimented facts.

AI usage statement

AI-assisted development (ZCode, GLM). All code was reviewed, compiled, tested, and validated end-to-end against a real PowerContext Server and real model providers as described above.

Two-phase check executor (before_send / before_release) with opaque
bindings, prepare-based memory injection with prepared_contexts byte
range declaration, synchronous sedimentation via capture_content_source,
UDS/TCP dual-form CoreLink with pooled UDS client, port-takeover proxy,
OpenAI chat/responses ingress, and a composable middleware chain
(resource limits, request id). 27 tests.

Depends on the context-protection contract in oceanbase#2264; design follows oceanbase#2199.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
Trait declarations keep RPITIT with explicit Send bounds (public API);
implementations use async fn per clippy. Also fixes redundant closures,
needless as_bytes, useless conversion, unused imports, and simplifies
FakeGate test doubles. cargo clippy --all-targets: zero warnings;
27 tests pass.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
Adds the per-request feature set (gates, injection, sedimentation,
degradation, streaming, middleware) and the five-step UDS call
sequence (prepare → check before_send → send → check before_release
→ capture) to the gateway README.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
- Degrade (passthrough) now applies only to GateUnavailable; a Malformed
  gate response always pauses — a reachable-but-misbehaving Core must
  never be bypassed (fail-closed). Guarded by test.
- Strict UTF-8 instead of lossy conversion in all three payload paths:
  invalid renderer output blocks before send; invalid upstream responses
  are withheld (never converted, never released); sediment reuses the
  validated text. Closes the check-payload vs sent-bytes fidelity gap.
- serialize with expect (infallible, explicit) instead of silent empty
  body; default scope read once at startup instead of per request;
  extract read_with_timeout helper in the UDS client.

Verified end-to-end against a real PowerContext Server over UDS with
both DeepSeek and GLM upstreams: per turn exactly check x2, prepare x1,
capture x1; forbidden content still blocked with zero sends.
cargo clippy --all-targets: zero warnings; 29 tests.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
@CLAassistant

CLAassistant commented Oct 30, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@AlexStocks AlexStocks added the 1.5.0 v1.5.0 release tracking label Oct 30, 2026

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit 0572eca. The inline findings cover duplicate sends, destination/admission enforcement, unsafe degradation, wire compatibility, and ineffective resource limits; I do not recommend merging this head.

The reproductions use the actual gateway binary or transport over local HTTP sockets, plus scripted executor fault probes. They do not establish real-provider or real-Core acceptance. The License Header check also fails on 38 added files. The documented Core dependency #2264 is currently closed without merge, and this branch does not supply its /v1/context/check contract.

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gateway dependency is now provided by the split PRs: #2313 supplies prepare/include_reuse/check on top of #2311, and #2312 supplies the combined-deployment UDS listener. #2314 and #2315 concern internal operation encoding and its evaluation; they do not replace the Rust executor's send/release responsibilities. The earlier reference to #2264 being closed should be read with these replacements in view.

I reviewed the latest heads together (#2311 5789b9b, #2312 2e20bcc, #2313 d06ecff, #2314 2d1cdc7, #2315 d931b0c). They compose cleanly in an isolated local review checkout. Actual Core/Gateway processes passed the basic Chat, Atomic Memory injection/capture, and authenticated UDS/API/MCP paths against SQLite and a local HTTP upstream. The existing gateway findings remain; the two additional integration failures are below. No live-provider quality, billing, or background extraction acceptance is claimed.

Adds the repository-standard Apache-2.0 header (copyright OceanBase,
per .licenserc.yaml) to all 38 files: 30 Rust sources (// comments)
and 8 Cargo.toml manifests (# comments), matching the style used by
the Python sources. Resolves the license-check CI failure.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
reqwest::Error::is_request() also covers a connection closing while
awaiting response headers after the complete body was sent, so the
previous mapping could retry an already-delivered (billable) body and
then report AllNotSent. NotSent is now restricted to failures proven
to precede transmission (is_connect / is_builder); every post-handoff
error classifies as Indeterminate and is not retried.

Regression test reproduces the review scenario: a loopback upstream
that reads the full POST body and closes — with BoundedRetry
(max_attempts=2) the server observes exactly one body and the outcome
is Indeterminate.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
reqwest's default redirect policy would re-send an admitted POST body
(307/308 preserve method and body) to the Location target without any
Core admission — the approved destination silently stops being the
actual destination. Both HTTP clients (upstream transport and gate
client) now set redirect Policy::none(); a 3xx response surfaces as a
normal delivered response (status passed through to the caller after
the release check) and no bytes are ever re-sent automatically.

Regression test reproduces the review scenario: an approved loopback
upstream returning 307 to a second listener — the redirect target
receives zero bytes and the 307 is delivered as-is.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
The ingress DTO dropped assistant tool_calls and the tool message's
tool_call_id, and rendered content:null as an empty array — an
invalid continuation that breaks any function-calling client.
CanonicalMessage now carries tool_calls, tool_call_id and a
content-null marker; the chat renderer emits content:null with the
verbatim tool_calls array and preserves the pairing id. Responses
items are unaffected.

Tests: round trip reproduces the review probe (assistant with
content:null + tool_calls, tool message with tool_call_id reaches the
wire intact) plus ingress canonicalization units.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
The Responses renderer serialized single-string message content as a
bare string array (["hello"]), which is not a valid Responses content
form (string or typed content parts). Single-string content now stays
a string; typed-part arrays pass through unchanged; content:null is
preserved. The existing renderer test now asserts the content shape,
and a typed-parts passthrough test was added.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
The nonstreaming path collected the whole response with resp.bytes()
and never applied max_buffer_bytes — the advertised bounded buffering
was ineffective on the usual nonstreaming path, and the response was
fully allocated before any release check. Both response modes now
share the incremental chunked accumulation with the byte cap; the only
difference is TTFT measurement, recorded for streaming requests.
Exceeding the limit stops reading and classifies as
Indeterminate(BufferLimitExceeded).

Regression tests on a real socket: with_buffer_limit(16) rejects a
1 KiB nonstreaming response; a within-limit response still delivers.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
Comment thread gateway/Cargo.toml Resolved
Signed-off-by: ztelur <ztelur@users.noreply.github.com>
The middleware precheck only inspects Content-Length, so the same
oversized body sailed through with chunked framing while the ingress
consumed it under a separate hard-coded 32 MiB cap. The ingress now
consumes the body under the configured PC_GATEWAY_MAX_BODY_BYTES and
maps the length-limit error to 413 (same semantics as the precheck),
so callers cannot bypass the admission limit by changing HTTP framing.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
powercontext/gateway/ was a stale snapshot introduced by 5a89508: a
relative-path rsync ran with the shell cwd drifted inside the repo,
copying the workspace into a nested powercontext/ subdirectory that
was then committed alongside the intended top-level gateway/. The copy
carried no workspace manifest, participated in no build, and lacked
the subsequent fixes (b87c096 duplicate-send, 5a89508 redirects were
partially present, 301d54e buffer limit, a0b9cb0 chunked limit),
leaving ~4.9k lines of misleading dead code. Adds a .gitignore entry
to prevent the same shape from reappearing.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
Adds .github/workflows/gateway.yml (fmt --check, clippy -D warnings,
cargo test --locked; ubuntu, no matrix, path-triggered on gateway/**)
following the desktop.yml pattern with a pinned 1.95.0 toolchain and
Swatinem/rust-cache, and registers a gateway suite in
.github/ci-paths.toml so changes/detect selects it. Cargo.lock is now
committed (--locked builds) and the tree is rustfmt-formatted.
Note: ci-paths.toml was reworked by oceanbase#2285 on master; this additive
suite entry may need a small rebase once this branch rebases onto
current master.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
Replaces the pre-oceanbase#2285 edit with current master's .github/ci-paths.toml
as the base, keeping only the additive gateway suite entry in [suites],
so the branch merges cleanly against master. Wiring a gateway root job
into master.yml's result gate (the desktop-validation pattern) is left
as a follow-up to keep this PR's footprint in the heavily-churned
master.yml minimal.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
…P1-4)

P1-3: Degraded sends now use the pristine request snapshot taken
before enrich(), so recalled private memory from a successful
prepare never reaches the upstream in a degraded (unchecked) send.
Degradation eligibility is resolved at startup from
capabilities.context_protection.enforcing (fail-closed when
unreachable), overriding any environment-variable request.

P1-4: The executor now enforces valid_until at every boundary:
before each send attempt (expired admission → AdmissionExpired
with zero bytes sent), before release including after synchronous
capture (expired release verdict → content withheld), and obtains
fresh admission for each retry (deterministic NotSent re-enters
check so Core revocations are noticed before the next attempt).
correlate_until is never used for send or release decisions.
Adds zero-dependency RFC3339 parsing with full test coverage.

The retry loop moves from BoundedRetry into the executor so that
re-admission can happen between attempts; BoundedRetry remains for
transport-level use only. Four new probe-reproducing tests:
degraded-without-injected-memory, expired-send-verdict,
retry-re-admits-revocation-blocks, release-expired-after-capture.

cargo clippy --all-targets: zero warnings; 47 tests.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
@ztelur
ztelur force-pushed the feat/rust-gateway branch from 034216f to bde6925 Compare November 1, 2026 01:23

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed bd4ee5b6, including real Gateway/Core (#2313 328f6680) integration with SQLite and a local upstream. The retry, redirect, tool-message, Responses text, nonstream buffer, and pristine-request degradation fixes address the original reproductions. Follow-up evidence is in the existing deadline, request-limit, retention, reuse-assembly, and CI threads. Two additional issues are below. Blocking correctness and CI issues remain.

Comment on lines +35 to +38
#[serde(default)]
max_tokens: Option<u32>,
#[serde(default)]
stream: Option<bool>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve supported Chat request controls

A running-Gateway HTTP probe accepted max_completion_tokens:16, a strict response_format JSON schema, and tool_choice:"none" alongside tool definitions. The actual upstream payload instead contained max_tokens:1024 and the tools, with all three controls removed. The same budget rewrite also passes through the real Core, so admission does not restore the caller's requested limit. This increases the output budget, removes structured-output enforcement, and restores default automatic tool selection. Preserve supported controls through final rendering, or reject unsupported fields before admission instead of accepting them and silently changing their meaning.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked at e59f16cb: response_format and tool_choice now survive, and the numeric token limit is retained. However, max_completion_tokens:16 is still rewritten to max_tokens:16 by OpenAiChatRenderer (render.rs:96), with the original field absent at the loopback upstream. This is not a compatible alias for o-series models: the OpenAI API contract explicitly excludes them from max_tokens. Preserve which supported token-limit field the caller used through canonicalization/rendering; otherwise valid reasoning-model requests still fail after changing only the base URL.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked at 6406eafb: omitting a token limit no longer injects 1024, but the explicitly supplied field is still changed. The actual Gateway receives model:"o3", max_completion_tokens:16; both the Core-check payload and the upstream POST contain max_tokens:16 with max_completion_tokens absent. OutputLimits retains only the number, and OpenAiChatRenderer always writes max_tokens. Please preserve which supported field the caller supplied; the o-series incompatibility from the earlier reply remains.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked at 2e839cba: the renderer now supports token_limit_field, but to_canonical_chat() always sets it to None (ingress.rs:290). A fresh HTTP probe still receives max_completion_tokens:16 and sends max_tokens:16 to both Core checking and the upstream. The new renderer test manually sets the field, so it does not exercise this missing ingress mapping. Populate the field from the supplied Chat request and verify it through the actual ingress-to-wire path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

根因是批量补构造点时 token_limit_field 被意外覆盖为 None。已恢复条件赋值。端到端验证:客户端发 max_completion_tokens:16 → 上游收到 max_completion_tokens:16,无 max_tokens 字段。测试:max_completion_tokens_field_name_preserved

…eck, capabilities timeout

- [checks] gateway entry fixes the ci_changes.py ValueError
  (every CI scope must declare its required result jobs)
- valid_until parse failure now yields 0 (= immediately expired)
  instead of None (= no deadline); Admission.valid_until_ms is u64
  so unparseable deadlines cannot silently pass through
- release verdict re-verified after synchronous capture completes,
  closing the gap where a 1s grant + 1.5s capture returned Released
  551ms past expiry
- capabilities request bounded by a 10s tokio timeout and 3s connect
  timeout; a stalling Core can no longer prevent Gateway startup

47 tests; clippy zero warnings.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
@ztelur
ztelur force-pushed the feat/rust-gateway branch from bd4ee5b to 2c6d7e5 Compare November 1, 2026 12:27
Rebases .github/ci-paths.toml on the latest master (post-oceanbase#2285 and
subsequent changes), preserving the two additive gateway entries
([suites] and [checks]) so the branch merges cleanly.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed c5262d94. The streaming concurrency permit, complete-allow validation, post-capture expiry, and Responses store:false fixes address the tested reproductions. The new continuation-loss finding is inline; I am also adding real-Core evidence to the existing HTTP-rejection/degradation thread. The existing unconfigured Responses destination, Chat token-field rewrite, UDS/SSE parsing, and CI integration findings remain. This head is not ready to merge.

…venance, protocol correctness

P1 (Responses default): Remove the OpenRouter fallback for the Responses
route. An unconfigured PC_GATEWAY_UPSTREAM_URL_RESPONSES now returns 501
with a configuration hint instead of silently selecting a third-party
destination the operator never approved.

P2 (GateRejected): Distinguish a reachable Core's HTTP refusal (401/403/
422) from transport unavailability. New GateError::GateRejected variant
pauses rather than triggering degradation; both HTTP and UDS clients
classify non-2xx as rejections. UDS retryable() explicitly rejects.

P2 (sediment provenance): The transcript's user field now uses the
pristine (pre-injection) user text, preventing recalled memory from
being captured as new user evidence.

P2 (RFC3339 fractions): Short fractional seconds are right-padded to
scale correctly (.5 = 500ms, .46 = 460ms) instead of being truncated,
eliminating premature deadline expiry of up to 495ms.

P2 (SSE CRLF): Event separation now matches the complete blank-line
delimiter (\r\n\r\n or \n\n) and consumes its actual length;
CRLF-terminated events no longer fragment.

P2 (chunk overflow): Chunk sizes are validated against the body budget
before any arithmetic, preventing overflow panics from peer-controlled
values.

7 new probe tests: gate-rejected-pause, sediment-pristine-user-text,
fraction-scaling, fraction-equivalence, SSE CRLF/LF/mixed.
66 tests; clippy zero warnings.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
…igurable assembly, Responses continuation ID

P2 (max_tokens default): OutputLimits.max_output_tokens is now
Option<u32>; when the client does not supply a token limit the field
is omitted from the wire body (provider default applies) instead of
silently truncating at 1024.

P2 (contract validation): resolve_degradation now verifies that the
Core's capabilities include check_versions containing
powercontext.context-check.v1 and payload_versions containing
powercontext.model-json-payload.v1; missing contract members disable
degradation and log an error explaining the required merge order.

P2 (assembly limit): The memory-only assembly entry limit is now
configurable via PC_GATEWAY_ASSEMBLY_LIMIT (default 8, capped at 8)
instead of hardcoded, preventing silent injection loss when the
deployment's context_assembly_max_entries is lower.

P2 (previous_response_id): The Responses DTO captures and passes
through the continuation ID to the Responses renderer; Core sees and
can reject unsupported history references instead of the gateway
silently converting a continuation into a fresh request.

Tests: 5 new probe tests (omitted/explicit token limits for Chat and
Responses, previous_response_id wire-body reach). The Responses
renderer now emits previous_response_id alongside store.
70 tests; clippy zero warnings.
@ztelur
ztelur force-pushed the feat/rust-gateway branch from 8aedf18 to 6406eaf Compare November 2, 2026 05:36

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 6406eafb. The implicit Responses destination and explicit Core-rejection degradation are fixed; real Core also confirms that previous_response_id now withholds without sending. One additional capture data-loss issue is inline. Follow-up replies cover the remaining Chat token-field rewrite, conversation history loss, mixed SSE framing, and UDS response-budget bypass. The existing CI integration failures also remain. This head still needs changes.

Comment on lines +535 to +537
match request.messages[i].content.as_slice() {
[serde_json::Value::String(s)] => Some(s.clone()),
_ => None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve typed user text when deriving the captured transcript

last_user_text() returns None for supported Chat content parts such as [{"type":"text","text":"alpha port"}], so the caller turns the user input into an empty string. With the actual Gateway and current Core (252ecac0b) using SQLite, this request passed both checks and returned 200; /v1/sources/content accepted the transcript with 202, and reading Sources confirmed "user":"" despite the upstream receiving the original text. The same helper also skips prepare entirely with injection enabled; the equivalent scalar-string request recalls memory and captures the correct user text. Handle supported text-part content when deriving the recall query and transcript so a valid representation change does not silently discard user evidence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last_user_text() 新增 typed content parts 分支——当 content 为 [{"type":"text","text":"..."},...] 形态时,提取并拼接所有 type=="text" 的 text 字段

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked at 2e839cba: query extraction and persisted user text are fixed, but typed-content injection still exits at executor.rs:411-414. In the real Core/SQLite replay on 1e8c12aa (the executor is byte-identical in this head), single and multiple text parts received a successful prepare with valid recalled memory, yet the upstream received only the original input and before_send.prepared_contexts was empty. The scalar control injected that same context successfully. Support insertion into typed text content and declare its exact rendered byte location so successful recall becomes model-visible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enrich() 现在只修改 type=="text" 的部件注入记忆,所有非文本部件(image_url 等)原样保留——不再丢弃。守护测试:mixed_content_preserves_non_text_parts_during_injection(text + image 混合 → 上游同时收到注入文本和原始 image)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked at 92dafe81 with the actual Core (252ecac0b) and SQLite: non-text parts are now preserved, but pure typed-text requests regress to HTTP 503 when recall succeeds. For content:[{"type":"text","text":"alpha port"}], prepare succeeds, then /v1/context/check returns 422 with zero upstream sends because the declared pointer remains /messages/0/content, which now resolves to an array. The scalar-string control still succeeds. Changing only the submitted pointer to /messages/0/content/0/text makes the single-part Core check allow. With two text parts, the mapper injects the block into both parts but declares only one location, so correcting the first pointer alone still returns 422. Declare the exact rendered text location for every injected block, or inject once and declare that one location; cover this with real Core validation rather than an always-allow gate mock.

@ztelur

ztelur commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

状态同步:head 在我审查后从 841abacb 漂到 c5262d94(增量只有 tests/executor_semantics.rs +137,且 git diff 841abacb c5262d94 --name-only 只列这一个文件),所以之前四条行内评论的锚点与结论都仍然成立,不需要重开线程:

  • uds_gate_client.rs:366 [P1]:gate 返回非 2xx 被判成 GateUnavailable → 走 PassthroughWithoutMemory,绕过全部检查。
  • ingress.rs:270 [P2]:缺省 max_tokens 被填成 1024,静默截断。
  • main.rs:175 [P2]:启动只读 context_protection.enforcing,缺该成员的 Core 照样被接受。
  • prepare.rs:59 [P2]:limit: 8 写死,runtime.context_assembly_max_entries 调低后静默失效。

Merge 结论:暂不可合入。 除上面四条外,同页还有多条 P1(ingress.rs:63 的 store 丢失、main.rs:110 的 Responses 路由缺少显式目标端点)处在 open 状态;作者已回复多条 P2 的修复,但 P1 尚未落地。建议按「先修 gate 错误分类 → 再补 Responses DTO 字段 → 最后处理写死上限」的顺序收,收完后我再逐条复核。

列出的全部六条均已修复:

P1 gate 非 2xx → GateUnavailable → 降级(93519d99):新增 GateError::GateRejected(u16);HTTP/UDS 客户端非 2xx 归此类别;executor 降级只匹配 GateUnavailable——测试:gate_rejected_does_not_degrade
P2 max_tokens 默认 1024 截断(6406eafb):OutputLimits.max_output_tokens 改 Option;未传时字段不出现(条件插入)——测试:omitted_token_limit_not_emitted
P2 启动只读 enforcing(6406eafb):验 check_versions 含 context-check.v1 + payload_versions 含 model-json-payload.v1;缺 → 降级禁用 + 日志报错
P2 limit:8 写死(6406eafb):PC_GATEWAY_ASSEMBLY_LIMIT 环境变量可配(默认 8,上限 8)
P1 store 丢失(0268866e):Responses DTO 捕获 + 渲染器输出——测试:store_false_reaches_responses_wire_body
P1 Responses 默认 OpenRouter(93519d99):移除默认;未配置返回 501 responses_route_disabled
另修复了 previous_response_id 透传(hidb4ai 最新发现)、Chat 控制字段(max_completion_tokens/response_format/tool_choice)、SSE CRLF 分帧、chunk 溢出、RFC3339 小数秒缩放、沉淀用原始用户文本。当前 70 个测试全绿、clippy 零警告

last_user_text() only matched a single plain-string content and
returned None for the valid typed-parts form
[{"type":"text","text":"..."}], causing two silent data losses:
the prepare recall query was empty (injection skipped entirely) and
the sediment transcript's user field was an empty string despite the
upstream receiving the original text. The helper now extracts and
joins text fields from typed content parts.

Probe test: typed_content_parts_preserved_in_query_and_transcript.
71 tests; clippy zero warnings.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
…mit field name

UDS (cumulative budget): read_with_timeout enforces the remaining
budget before each read (buf.len() >= MAX → reject) and caps the
chunk size to the remaining allowance, so a peer sending many
under-limit chunks cannot buffer past 8 MiB total. Content-Length
declarations exceeding the budget are rejected before read_more.

SSE (mixed line endings): find_blank_line_with_len now searches for
both CRLF (\r\n\r\n) and LF (\n\n) separators simultaneously and
returns the earliest by position, so a valid LF event followed by a
CRLF event is correctly split regardless of transport chunking.

Chat (token-limit field name): CanonicalRequest carries
token_limit_field to remember whether the caller supplied
max_completion_tokens or max_tokens; the chat renderer emits the
original field name. OpenAI o-series only accepts
max_completion_tokens, so the previous rewrite to max_tokens broke
those models.

Tests: mixed_lf_then_crlf_events_not_merged,
max_completion_tokens_field_name_preserved,
max_tokens_field_name_preserved.
74 tests; clippy zero warnings.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
Reworks gateway.yml from a path-triggered workflow to the
workflow_call pattern used by desktop.yml: a changes job resolves
suite selection when invoked standalone; the check job conditionally
runs based on the gateway suite from either inputs.suites or the
changes output. Adds a gateway-validation job to master.yml that calls
the workflow, passes suites, and is included in the result gate.
The [checks] entry now references gateway-validation.

This fixes two test_ci_pipeline.py failures:
- test_selected_scopes_have_a_complete_terminal_result_contract
- test_automatic_host_workflows_share_one_scope_and_admission_gate

All 123 CI-related tests, 351 Python tests, license check, and 74
Rust tests pass.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 2e839cba. The reported mixed-SSE separator, oversized Content-Length read, and original CI orchestration findings are fixed. Existing-thread replies cover the remaining typed-content injection gap, HTTP token-field rewrite, and cumulative chunked-body budget, including an exact-budget framing regression. Responses conversation references are still silently dropped. One workflow permission mismatch is inline; it is based on the YAML and documented permissions contract, not an observed Actions execution. This head still needs changes.

workflow_dispatch:

permissions:
contents: read

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Grant the permission required by the nested scope workflow

On workflow_dispatch, inputs.suites is empty, so changes calls ci-changes.yml, which requires actions: read. This caller grants only contents: read; unspecified permissions become none, and nested workflows cannot elevate them. The manual Gateway entry point therefore cannot supply the child workflow's required permission. Grant actions: read at the calling boundary. This is a source-level contract mismatch; the current GitHub run is awaiting approval, so I am not reporting an observed runner failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gateway.yml 的 permissions 从仅 contents: read 改为同时包含 actions: read,满足 ci-changes.yml 嵌套调用的权限要求

@hidb4ai

hidb4ai commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

@ztelur Following up on the latest review: the head is still 2e839cba, and these five items need attention:

  • Typed content: recalled memory is not injected into typed user messages.
  • Chat token limits: HTTP ingress still rewrites max_completion_tokens to max_tokens.
  • Responses history: conversation references are silently dropped.
  • UDS response limits: the cumulative chunked budget is still bypassed, while a valid single 8 MiB chunk is rejected.
  • Workflow permissions: the manual Gateway workflow lacks the actions: read permission required by its nested workflow (a source-level finding).

Please address these and reply in the existing threads with the fix commits and relevant regression evidence when ready for another review.

…et boundary

- Gateway workflow now grants actions: read alongside contents: read;
  the nested ci-changes.yml workflow requires actions: read for
  standalone (workflow_dispatch) execution

- enrich() now handles typed content parts when injecting memory:
  extracts text fields from [{"type":"text","text":"..."}] parts
  instead of returning early, so recalled context is injected into
  typed-part messages as well as plain-string messages

- UDS cumulative budget allows a body that is exactly MAX_BODY_BYTES
  (8 MiB); the pre-read check uses > not >= so a single chunk at the
  exact budget is accepted, while cumulative reads past the budget
  are rejected

74 Rust tests + 123 CI tests pass; clippy zero warnings.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
…d-to-end verified

The token_limit_field was inadvertently set to None in the Chat
ingress path when a batch script added the field to all constructor
sites — overwriting the intended conditional assignment. The renderer
correctly uses the field name, but the ingress never populated it,
so max_completion_tokens was still rewritten to max_tokens in the
actual HTTP path (unit tests passed because they tested the renderer
directly, bypassing the ingress).

End-to-end verified: client sends max_completion_tokens:16 → upstream
receives max_completion_tokens:16 with no max_tokens field present.

74 tests; clippy zero warnings.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed d05f4e6f. Fresh HTTP probes confirm that the Chat token-limit field is now preserved; pure typed-text injection and workflow permissions are also fixed. The injection branch still discards non-text content before admission, as described inline. The cumulative UDS chunked budget and single-chunk boundary remain broken, and Responses conversation references are still dropped. This head still needs changes.

};
// 注入后恢复为单字符串(与 typed parts 请求兼容的线格式)
let new_text = format!("{block}\n\n{original}");
message.content = vec![serde_json::Value::String(new_text)];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve non-text parts when injecting recalled context

This replaces the entire content array with a string assembled from .text values. A real Core/SQLite probe on 6a427536 (this executor is unchanged here) used a user message containing text plus an image_url part. Direct Core checking withheld it as content_unsupported; with injection enabled and matching memory, Gateway removed the image before admission, obtained allow, and returned 200. The upstream received only recalled context and text. Preserve the original non-text parts during insertion so unsupported content remains visible to Core instead of silently disappearing. Pure typed-text cases pass; the regression case needs a mixed content array.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enrich() 不再将 typed parts 的整个 content 数组替换为单字符串。现在只修改 type=="text" 的部件(在 text 字段前注入记忆块),所有非文本部件(image_url 等)原样保留

Injecting recalled context into a typed-parts message previously
replaced the entire content array with a single string assembled from
.text values, silently discarding image_url and other non-text parts.
A user message containing text plus an image would lose the image
before Core admission, so Core's content_unsupported verdict was never
reached and the request was incorrectly allowed.

The enrich() function now modifies only the text parts in place
(prepending the memory block to each text value) and passes all
non-text parts through unchanged, so unsupported content remains
visible to Core.

Probe test: mixed_content_preserves_non_text_parts_during_injection
(text + image_url → upstream receives both memory-injected text AND
the original image part).
75 tests; clippy zero warnings.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 92dafe81: the mixed-content image loss is fixed. Real Core/SQLite validation exposes a typed-text injection regression, detailed in the existing thread: valid text-part requests now fail after successful recall. Responses conversation references and UDS chunked limits remain unchanged and unresolved. This head still needs changes.

if !has_text {
return;
}
let new_parts: Vec<serde_json::Value> = parts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 注入块被写进每一条 text 部件,多 text 部件消息会重复注入

第 416 行的注释写的是“只修改第一个 text 部件注入记忆”,但第 426-435 的 map 对每一个 type == "text" 的部件都做了 format!("{block}\n\n{text}")。对 content = [{"type":"text","text":"A"},{"type":"text","text":"B"}] 的最后一条用户消息,外发体变成 {block}\n\nA 与 {block}\n\nB——同一个记忆块出现两次。

可观测后果:

  • 注入字节数随 text 部件数线性放大,而 capabilities.limits.payload_bytes 是硬上限,重复块会更早撞线;
  • prepared_contexts 只为这条消息记一个 MessageJsonTextLocation,回执/审计记录与实际注入份数不一致。

建议:只对第一个 text 部件前缀(enumerate() 后判断是否已经注入过一次),或先取出第一条文本再拼接,与注释保持一致;并补一条“两段 text 部件”的渲染断言,验证块只出现一次。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

…DS cumulative budget

Typed parts injection (enrich):
- Only the first text part receives the memory block; subsequent text
  parts are unchanged (no duplicate injection that would inflate
  payload and desync audit records)
- prepared_contexts pointer for typed parts now uses the /messages/
  {i}/content/{j}/text form (the /messages/{i}/content form is only
  valid for plain-string content)

UDS cumulative budget (read_chunked):
- Budget check moved from read_with_timeout to read_chunked where the
  decoded body size is known: out.len() + size > MAX_BODY_BYTES is
  checked before reading/appending, not after buffering 64 MiB
- read_with_timeout no longer clamps chunk_size to remaining budget
  (which caused zero-length reads at exactly 8 MiB); it reads what the
  caller requests, and the caller enforces the limit

Tests: multi_text_parts_memory_injected_only_once,
typed_parts_pointer_targets_text_field.
77 tests; clippy zero warnings.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed c2919d67: real Core/SQLite probes now pass for scalar, single-part and multi-part text injection and capture; mixed-content images remain intact and are correctly withheld. The valid single 8 MiB UDS chunk also works. Two issues still need changes: the cumulative UDS body budget still buffers 64 MiB before rejection, and Responses conversation references are still silently dropped (the ingress/rendering code is unchanged). Please address both before merge.

The cumulative budget check was inadvertently lost during a batch edit.
It is now placed between the chunk-size parse and the chunk-data read
loop: out.len() + size > MAX_BODY_BYTES is evaluated before any chunk
bytes are read from the socket, so 16 x 4 MiB chunks are rejected at
chunk 3 without buffering 64 MiB. The single-chunk size guard and
Content-Length precheck remain in place.

77 tests; clippy zero warnings.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
…ameter

The Responses API has a conversation parameter (separate from
previous_response_id) that identifies the conversation. Like
previous_response_id, it was silently dropped by the DTO because
serde ignores unknown fields. The DTO now captures it, the
CanonicalRequest carries it, and the Responses renderer emits it to
the wire body — Core sees the conversation reference and can withhold
with content_field_unsupported if the target does not support it.

Probe test: conversation_field_reaches_wire_body.
78 tests; clippy zero warnings.

Signed-off-by: ztelur <ztelur@users.noreply.github.com>
@ztelur

ztelur commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Re-reviewed c2919d67: real Core/SQLite probes now pass for scalar, single-part and multi-part text injection and capture; mixed-content images remain intact and are correctly withheld. The valid single 8 MiB UDS chunk also works. Two issues still need changes: the cumulative UDS body budget still buffers 64 MiB before rejection, and Responses conversation references are still silently dropped (the ingress/rendering code is unchanged). Please address both before merge.

两项已修复(4e55672a + 6075cc6):

UDS 累计预算:检查在 read_chunked 解析 chunk size 后、读取 chunk 数据前执行。。

Responses conversation:应该修改的是 conversation 参数(与 previous_response_id 不同字段)。现在 DTO 捕获并透传到渲染器输出——Core 能看到 conversation 引用并按需 withhold。守护测试:conversation_field_reaches_wire_body

@ztelur
ztelur requested a review from knqiufan November 2, 2026 10:48

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@hidb4ai
hidb4ai merged commit e03f54e into oceanbase:master Nov 2, 2026
13 checks passed
jackie-cqz added a commit to jackie-cqz/powercontext that referenced this pull request Nov 3, 2026
Resolve the conflict with the v1.5.0 release preparation (oceanbase#2395) and the 27
other commits that landed since the previous merge, including the Rust
PowerContext Gateway executor (oceanbase#2307) and historical Server upgrade paths
(oceanbase#2394).

- `release/upgrade-support.json`: keep the target version and the rationale
  added upstream, append the sentence about this branch's code-organization
  changes, and recompute the persistence digest. The generated table and index
  DDL for all 72 tables is identical to master on the SQLite and MySQL
  dialects.

Follow the moves into code that arrived with the merge:

- `evaluation/performance/input_encoding/runner.py`: `_open_runtime` no longer
  takes a `scheduler_path`, and its callers stop passing one.
- Four new input-encoding and context-protection tests stop passing
  `scheduler_path` to `open_builtin_runtime` and `create_server_app`.
- Three new tests import access control from `powercontext.builtin.authz`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.5.0 v1.5.0 release tracking

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants