Skip to content

Review only: HOUSE-79 upstream v8.0.44, part 8 of 13: management handlers and small translators - #20

Closed
ericlitman wants to merge 1 commit into
review/house79/base-08from
review/house79/chunk-08
Closed

ericlitman wants to merge 1 commit into
review/house79/base-08from
review/house79/chunk-08

Conversation

@ericlitman

@ericlitman ericlitman commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Review only. Do not merge. This PR is one part of the review of #13, the merge of upstream v8.0.44 into the fork. That PR's diff (966 files) is too large for one review round.

  • This part covers: management handlers and small translators.
  • The head tree is identical to Merge upstream v8.0.44 into the fork (HOUSE-79) #13 head tree (c88d701f7bc0894cde517ca7f931f5c05749cd74). The base is that tree with only this part's files restored to main (c265779f), so the diff is this part's files alone and the surrounding code is the real final code.
  • The 13 parts together cover every production file the sync changes (410 paths). Tests, golden data, generated model catalogs, images, docs and go.sum are in no part's diff.
  • The code is upstream's, except where our fork's soonest-reset selector, quota observations, quota reserves, usage reads and Terra injection touch it. Review it as code we are about to deploy to the proxy every one of our agents uses: report defects that would affect our deployment.

Linear: HOUSE-79

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@mergify

mergify Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again.

@unfret

unfret Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

🛑 9 blocking findings

🔴 Critical · Nil Devin handler now panics before its guard

🔴 Critical · Clone OAuth configuration under the handler lock

🟡 Medium · Direct Responses conversion discards translation failures

🟡 Medium · Evaluate dropped attachments across the merged user turn

🟡 Medium · `tool_choice: none` leaves native tools enabled

🟡 Medium · Honor the credential proxy during SIWC binding

🟡 Medium · Per-session proxy makes optional project lookup fatal

🔵 Low · Limit plugin default metadata to v8 responses

🔵 Low · Keep plugin model fallback exclusive to v8

Walkthrough

This is a review-only slice (part 8 of 13) of the upstream v8.0.44 merge into the fork, covering management API handlers and the Antigravity/Interactions request and response translators; it is not meant to be merged on its own. On the management side, OAuth logins can now carry a per-session proxy, the new v8 endpoints add Codex SIWC binding, and config edits now happen under the handler lock with stale-reload protection and If-Match/ETag checks. On the translator side, request translators can now return an error, so a user turn whose only attachment can't be sent upstream is rejected instead of forwarded empty, and the Antigravity paths get several fixes to tool arguments, usage accounting and finish reasons.

Area What changes Review notes
OAuth sessions and per-session proxy `StartOAuthV8` accepts a POST JSON body with `provider`, `proxy_url`, `domain`, `is_webui` and extra metadata. A validated `proxy_url` is threaded into each provider's auth service, stored on the session and on saved records, and redacted from logs and errors. Saves now atomically claim a committing state so a concurrent cancel cannot race persistence. Plugin poll failures go through the same state machine. Clone OAuth configuration under the handler lock
Nil Devin handler now panics before its guard
Per-session proxy makes optional project lookup fatal
Codex SIWC binding New v8 endpoints start a SIWC bind for an existing file-backed Codex OAuth credential, accept the pasted loopback callback, exchange the code and bind via the auth manager. Legacy callback sinks refuse SIWC states. v8 uploads and deletes of the target file invalidate pending binds. Auth-file listings expose SIWC status fields on v8. Honor the credential proxy during SIWC binding
Config and auth-file management The handler keeps a cloned config and ignores reloads parsed from an outdated file. Field and list setters now mutate and persist under `h.mu`. The v8 config endpoint returns an ETag and honors If-Match. Body-read errors map through `clienterror`, so oversized bodies return 413. Invalid auth-file uploads (including bad `excluded_models`) return 400. Status and field patches go through `authManager.Modify` with the registration epoch. Quota support uses plugin IDs, and declarative `quota_probe` is hidden on v8. Plugin config fields expose defaults, and static model definitions fall back to plugin and registry models. Limit plugin default metadata to v8 responses
Keep plugin model fallback exclusive to v8
Translator error plumbing and dropped attachments `RequestTransform` now returns `(\[\]byte, error)`, and the registry stores the error on `RequestEnvelope.Err`. Shared `UserTurnDrops` and `UserRun` helpers refuse a user turn left with nothing sendable after unsupported image, file or document parts are removed. The OpenAI Responses→Antigravity request transform is now registered as nil in favor of the envelope path. Direct Responses conversion discards translation failures
Evaluate dropped attachments across the merged user turn
Antigravity and common translator behavior Antigravity changes: - Claude tool-call args are cleaned, with deferred empty `{}` input. - Usage math accounts for cached prompt tokens and adds thought tokens to completion tokens. - Safety-type finish reasons map to `content_filter`. - Upstream function-call IDs are preserved. - Interactions streams complete only on usage at or after the finish frame. - `tool_choice: none` no longer strips tools. Shared helpers: - `cache_control` skips empty and thinking blocks. - New helpers cover Codex replay signatures and a native Messages JSON-to-SSE adapter. `tool_choice: none` leaves native tools enabled
Advisories (1)
  • 🔵 Low · JSON-to-SSE adapter duplicates text-block citations · internal/translator/common/claude_native_response.go:40-61
    In ClaudeMessagesJSONToSSE the text case blanks only `text` in the content_block_start payload (line 40). The block's full `citations` array stays in that start event. Lines 58-63 then re-emit every citation as a separate citations_delta. Under Anthropic stream semantics, a consumer pushes the start block and appends each citations_delta to that block's `citations` list; the Anthropic SDK accumulator works this way. Trigger: a native Messages JSON response whose text block carries N citations, for example from web search or a document with citations enabled. Result: the accumulated message ends with 2N citations. The other block types avoid this: tool_use resets `input` to {}, and thinking resets `thinking` and `signature` before the deltas re-supply them. Citations get no such reset, so they are applied twice. Also reported for this defect: - Native Claude citation blocks emit citations twice (internal/translator/common/claude_native_response.go:40): For a native Messages text block containing citations, the synthesized `content_block_start` clears only `text` and retains the complete `citations` array. The function then emits every citation again as a `citations_delta`, causing aggregators to duplicate citations or strict clients to reject the malformed stream.
    Not blocking because: The duplicate citation representation adds internal serialization overhead only in the identified consumers: they do not forward this SSE to clients, and the Responses accumulator reads citations solely from deltas.
Since the last review
  • New: Limit plugin default metadata to v8 responses
  • New: Direct Responses conversion discards translation failures
  • New: Keep plugin model fallback exclusive to v8
  • New: Evaluate dropped attachments across the merged user turn
  • New: Nil Devin handler now panics before its guard
  • New: `tool_choice: none` leaves native tools enabled
  • New: Clone OAuth configuration under the handler lock
  • New: Honor the credential proxy during SIWC binding
  • New: Per-session proxy makes optional project lookup fatal
Repair prompt

Read the Unfret check run for mobilyze-llc/CLIProxyAPI#20 at head a02c799031563bac0f81491e43d80b550ca62974 (run panel:a02c799031563bac0f81491e43d80b550ca62974:YVv1kcSn2Pt:ZHRCjVyN9c0, check). Use the check's App id and GET /repos/mobilyze-llc/CLIProxyAPI/check-runs/122334664381; its output.text contains the structured result. Compare it with the current code and head, verify that each listed mandatory finding still applies, repair only those that do, honor the repository constraints, and validate the changes. Leave advisories and accepted risks alone. A manually resolved thread is not a waiver.

Details
Lane / stage Model Effort Outcome Duration
broad-gpt-5.6-sol-xhigh / review openai/gpt-5.6-sol → gpt-5.6-sol · primary xhigh succeeded 8 min 47 s
broad-opus-5.5-max / review anthropic/claude-opus-5-5 → claude-opus-5-5 · primary max succeeded 16 min 5 s
focal-gpt-5.6-sol-xhigh copy / review openai/gpt-6.1-sol → gpt-6.1-sol · primary xhigh succeeded 16 min 37 s
focal-gpt-5.6-sol-xhigh copy 2 / review openai/gpt-6.1-sol → gpt-6.1-sol · primary xhigh succeeded 14 min 42 s
judge / judge openai/gpt-6-astra → gpt-6-astra · primary high succeeded 2 min 44 s

First review of this pull request.
4 reviewers finished.
Commit a02c799.
Check run.
Run: GET /unfret/run/panel:a02c799031563bac0f81491e43d80b550ca62974:YVv1kcSn2Pt:ZHRCjVyN9c0.

@unfret review · @unfret status · @unfret help

@unfret unfret Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛑 9 blocking findings
🔴 Critical · Nil Devin handler now panics before its guard
🔴 Critical · Clone OAuth configuration under the handler lock
🟡 Medium · Direct Responses conversion discards translation failures
🟡 Medium · Evaluate dropped attachments across the merged user turn
🟡 Medium · `tool_choice: none` leaves native tools enabled
🟡 Medium · Honor the credential proxy during SIWC binding
🟡 Medium · Per-session proxy makes optional project lookup fatal
🔵 Low · Limit plugin default metadata to v8 responses
🔵 Low · Keep plugin model fallback exclusive to v8
Check

out = append(out, pluginConfigFieldInfo{
Name: htmlsanitize.String(field.Name),
Type: htmlsanitize.String(string(field.Type)),
Default: htmlsanitize.JSONValue(field.Default),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low · Limit plugin default metadata to v8 responses

This shared mapping is used by ListPlugins at GET /v0/management/plugins, so adding Default also changes the deprecated v0 response, including nested metadata.config_fields. AGENTS.md prohibits feature additions to /v0/management unless required for compilation; exposing plugin defaults is not a compilation repair. Keep the new response field confined to v8. Also reported for this defect: - Keep plugin defaults out of the legacy response schema (internal/api/handlers/management/plugins.go:54): ListPlugins serializes this shared struct for both API versions, so the unconditional default key, populated at line 473, extends GET /v0/management/plugins as well as v8. AGENTS.md explicitly forbids such legacy-endpoint feature changes except compilation fixes. Project defaults only into the v8 response while preserving the v0 schema.

Also affects:

Stream: stream,
})
return req.Body
return req.Body, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Direct Responses conversion discards translation failures

If envelope conversion sets req.Err, such as when an unsupported attachment empties a user turn, this wrapper still returns the translated body with a nil error. Direct callers therefore cannot reject the request and may send the attachment-stripped body upstream despite the new request-transform error contract. Also reported for this defect: - Return translation refusals from the Responses wrapper (internal/translator/antigravity/openai/responses/antigravity_openai-responses_request.go:136): Calling this converter with a user turn containing only an input_file file_id produces an UnsupportedPartError in req.Err, but this return discards it. The exported function consequently reports success with request.contents empty, unlike its envelope counterpart and the other error-returning translators. A direct-call probe reproduced the lost refusal; return req.Err alongside req.Body. - Return the envelope's translation error (internal/translator/antigravity/openai/responses/antigravity_openai-responses_request.go:136): For a user turn containing only an input_file with a file_id, the envelope converter sets UnsupportedPartError, but this wrapper returns nil instead. Callers using the newly error-returning function therefore see success and receive a payload with the user content removed, bypassing the request-scoped refusal. Return req.Err alongside req.Body.

models := registry.GetStaticModelDefinitionsByChannel(channel)
if models == nil {
if len(models) == 0 {
models = h.pluginModelDefinitionsByChannel(channel)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low · Keep plugin model fallback exclusive to v8

This handler also serves GET /v0/management/model-definitions/:channel. For a plugin-only channel, the new unconditional fallback changes that legacy endpoint from an unknown-channel error to a dynamic model response. AGENTS.md forbids feature changes under /v0/management unless required for compilation; the new fallback needs to be restricted to v8. Also reported for this defect: - Keep dynamic model discovery off the deprecated v0 endpoint (internal/api/handlers/management/model_definitions.go:27): GetStaticModelDefinitions is still registered at GET /v0/management/model-definitions/:channel, so this unconditional fallback adds plugin/runtime model discovery to that deprecated endpoint. AGENTS.md expressly prohibits feature changes under /v0/management except compilation repairs. Scope the new discovery behavior to the v8 endpoint while preserving the v0 contract.


if originalRole == "user" {
// Whitespace-only text is forwarded but never keeps an emptied turn alive.
drops.EndTurn(translatorcommon.CountSendableGeminiParts(partItems))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Evaluate dropped attachments across the merged user turn

With two adjacent Claude user messages—one containing text and the next containing an unsupported document file_id—this call records an emptied turn and returns UnsupportedPartError. Later, MergeAdjacentGeminiContents combines those messages into one nonempty user turn. The request consequently receives HTTP 400 even though the shared policy permits dropping an attachment when that translated turn still contains sendable text.

// http://127.0.0.1:<port>/callback (http protocol, 127.0.0.1 host, and /callback path).
func (h *Handler) devinCallbackURL() (string, error) {
if h == nil || h.cfg == nil || h.cfg.Port <= 0 {
cfg := h.configSnapshot()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Critical · Nil Devin handler now panics before its guard

When RequestDevinToken is invoked with a nil *Handler, devinCallbackURL now calls h.configSnapshot() before checking h == nil. Since configSnapshot dereferences h.mu, this crashes instead of returning the existing "server port is not configured" error.

@@ -520,9 +529,6 @@ func applyOpenAIToolChoiceToAntigravity(out, rawJSON []byte, functionNameMap map
}

out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", mode)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · tool_choice: none leaves native tools enabled

The deleted if mode == "NONE" { out, _ = sjson.DeleteBytes(out, "request.tools") } guard leaves googleSearch, codeExecution, and urlContext blocks in the Antigravity request. functionCallingConfig.mode = NONE only controls function calling, so a request that explicitly disables tools can still execute one of these native tools.

SDKConfig: config.SDKConfig{ProxyURL: proxyURL},
}
}
cloned := cfg.CloneForRuntime()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Critical · Clone OAuth configuration under the handler lock

Starting OAuth with a nonempty proxy_url while another request updates a configuration map, such as PATCH /v0/management/oauth-excluded-models, can terminate the process with "fatal error: concurrent map iteration and map write". configSnapshot releases h.mu before returning the shared configuration, so CloneForRuntime iterates its maps without synchronization. This crash was reproduced with concurrent cloning and locked configuration writes. Also reported for this defect: - Clone OAuth configuration under the handler lock (internal/api/handlers/management/handler.go:164): Starting OAuth with a non-empty proxy_url concurrently with PatchOAuthExcludedModels can terminate the entire server. configSnapshot returns the live h.cfg pointer after releasing h.mu, so CloneForRuntime iterates its maps while the other handler mutates them. A concurrency probe reproduced fatal error: concurrent map iteration and map write. Capture the clone while holding h.mu.

c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initialize SIWC authorization"})
return
}
var service siwcOAuthService = codex.NewCodexAuth(cfg)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Honor the credential proxy during SIWC binding

For an existing Codex credential reachable only through target.ProxyURL, binding creates its OAuth client using only the global proxy configuration. The authorization URL is returned successfully, but callback token exchange attempts the wrong network route and fails. Normal Codex refresh already honors auth.ProxyURL through NewCodexAuthWithProxyURL; the new binding path needs the same credential-specific proxy selection.

Comment on lines +484 to +487
if sessionProxyURL != "" {
log.Errorf("antigravity: failed to fetch project ID: %s", redactSessionProxyInString(errProject.Error(), sessionProxyURL))
SetOAuthSessionError(state, "Failed to fetch project ID")
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Per-session proxy makes optional project lookup fatal

When Antigravity token exchange and user-info lookup succeed but FetchProjectID fails, supplying a per-session proxy now aborts the entire OAuth flow. The same lookup failure without a session proxy remains non-fatal and the credential format already supports an empty project ID, so users whose proxy cannot reach that optional endpoint lose an otherwise valid credential.

@ericlitman

Copy link
Copy Markdown
Contributor Author

Closing: the review scope changed. Eric (2026-10-27): "i don't want to fix anything that's an issue in upstream - our only goal here is to validate our changes". Upstream code is out of scope. Our fork's own changes on v8.0.44 get one review PR instead. Findings from this part are recorded in the HOUSE-79 adjudication record. None was a defect in our code.

@ericlitman ericlitman closed this Oct 27, 2026
@ericlitman
ericlitman deleted the review/house79/chunk-08 branch October 27, 2026 19:16
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.

1 participant