Skip to content

Review only: HOUSE-79 upstream v8.0.44, part 11 of 13: plugin host, home, commands - #23

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

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

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: plugin host, home, commands.
  • 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

🛑 13 blocking findings

🔴 Critical · Keep stream usage buffering bounded

🔴 Critical · Do not trust a pre-existing shared shadow directory

🔴 Critical · Persist SIWC binding to the selected storage backend

🟡 Medium · Home latency RPC adds a prohibited post-connect timeout

🟡 Medium · Unix plugin loading now depends on an executable, writable os.TempDir

🟡 Medium · Do not carry the probe deadline past connection setup

🟡 Medium · Preserve origin-relative native library dependencies

🟡 Medium · Suffix parsing can produce inconsistent plugin model IDs

🔵 Low · Handle the deferred probe-client close error

🔵 Low · Handle the deferred source-file close error

🔵 Low · Leave deprecated management endpoint behavior unchanged

🔵 Low · Handle the deferred verification-file close error

🔵 Low · Preserve structured diagnostic fields

Walkthrough

This review-only slice of the upstream v8.0.44 sync covers the plugin host, the Home control-plane client, and command-line entry points. The plugin host now loads every native plugin from a content-addressed shadow copy and never unloads it, and it can decode compressed request bodies (with size limits) for frontend auth plugins. It also lets request interceptors skip receiving the body (schema version 7), applies thinking and payload rules before plugin executors run, and adds a host.auth.models callback. The Home client gains planned cluster rebalancing and latency telemetry, while the commands add a SIWC credential-binding flag, change model-catalog startup and discovery output, and change the Vertex import default location to global. Findings include critical concerns about unbounded stream usage buffering, a trusted shared shadow directory, and SIWC bindings that skip the configured storage backend.

Area What changes Review notes
Plugin loading and lifecycle Unix and Windows loaders load from per-process shadow copies, never dlclose/FreeLibrary, keep host callback blocks resident, and clean stale directories from dead PIDs. Plugins that config no longer enables are detached on reload. Do not trust a pre-existing shared shadow directory
Unix plugin loading now depends on an executable, writable os.TempDir
Preserve origin-relative native library dependencies
Handle the deferred source-file close error
Handle the deferred verification-file close error
Plugin request/response paths Bounded zstd decoding gives auth plugins a DecodedBody, and those plugins can now return explicit denials. Executor calls apply suffix-stripped models, thinking config, and payload rules. Token-count replies are translated as counts, and the 64 KiB stream line-buffer flush is removed. Structured stream errors carry status and Retry-After. Cross-origin redirects strip credential headers. Keep stream usage buffering bounded
Suffix parsing can produce inconsistent plugin model IDs
Plugin auth and models Catalog-only plugins no longer register model clients independently of credentials. Auth-file saves go through plugin ParseAuths (possibly producing several virtual auths) and remove stale records for the same file. OAuth login can use a session proxy, and quota lookups now map each provider to the plugin that serves it. Leave deprecated management endpoint behavior unchanged
Home client Adds a rebalance protocol (ENABLE/CLAIM/RESULT with a preflight PING), RTT sampling and latency reports, and separate fencing for dispatch and release. Subscription receiving moves into a goroutine with bounded backpressure, and RESP certificate reads get size limits. Home latency RPC adds a prohibited post-connect timeout
Do not carry the probe deadline past connection setup
Handle the deferred probe-client close error
Preserve structured diagnostic fields
Commands Adds the -siwc-bind OAuth flow, which writes SIWC tokens into an auth-dir Codex file. Remote catalog updaters are replaced by registry.SetLocalModelCatalogs, and seeded git-backed configs are prepared before upload. LAN discovery output is reworked. The fetch tools force a gpt-5.5 baseline and handle Devin lightning variants. Persist SIWC binding to the selected storage backend
Since the last review
  • New: Home latency RPC adds a prohibited post-connect timeout
  • New: Keep stream usage buffering bounded
  • New: Handle the deferred probe-client close error
  • New: Do not trust a pre-existing shared shadow directory
  • New: Unix plugin loading now depends on an executable, writable os.TempDir
  • New: Handle the deferred source-file close error
  • New: Do not carry the probe deadline past connection setup
  • New: Leave deprecated management endpoint behavior unchanged
  • New: Persist SIWC binding to the selected storage backend
  • New: Preserve origin-relative native library dependencies
  • New: Handle the deferred verification-file close error
  • New: Suffix parsing can produce inconsistent plugin model IDs
  • New: Preserve structured diagnostic fields
Repair prompt

Read the Unfret check run for mobilyze-llc/CLIProxyAPI#23 at head f5f1568777105fd88751089c4a86afa488ba4f9f (run panel:f5f1568777105fd88751089c4a86afa488ba4f9f:VTQm_hT2P4a:URXit1TYL0Y, check). Use the check's App id and GET /repos/mobilyze-llc/CLIProxyAPI/check-runs/122368259966; 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 16 s
broad-opus-5.5-max / review anthropic/claude-opus-5-5 → claude-opus-5-5 · primary max succeeded 17 min 44 s
focal-gpt-5.6-sol-xhigh copy / review openai/gpt-6.1-sol → gpt-6.1-sol · primary xhigh succeeded 16 min 20 s
focal-gpt-5.6-sol-xhigh copy 2 / review openai/gpt-6.1-sol → gpt-6.1-sol · primary xhigh succeeded 16 min 39 s
judge / judge openai/gpt-6-astra → gpt-6-astra · primary high succeeded 3 min 26 s

First review of this pull request.
4 reviewers finished.
Commit f5f1568.
Check run.
Run: GET /unfret/run/panel:f5f1568777105fd88751089c4a86afa488ba4f9f:VTQm_hT2P4a:URXit1TYL0Y.

@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.

🛑 13 blocking findings
🔴 Critical · Keep stream usage buffering bounded
🔴 Critical · Do not trust a pre-existing shared shadow directory
🔴 Critical · Persist SIWC binding to the selected storage backend
🟡 Medium · Home latency RPC adds a prohibited post-connect timeout
🟡 Medium · Unix plugin loading now depends on an executable, writable os.TempDir
🟡 Medium · Do not carry the probe deadline past connection setup
🟡 Medium · Preserve origin-relative native library dependencies
🟡 Medium · Suffix parsing can produce inconsistent plugin model IDs
🔵 Low · Handle the deferred probe-client close error
🔵 Low · Handle the deferred source-file close error
🔵 Low · Leave deprecated management endpoint behavior unchanged
🔵 Low · Handle the deferred verification-file close error
🔵 Low · Preserve structured diagnostic fields
Check

Comment on lines +230 to +231
commandCtx, cancelCommand := context.WithTimeout(ctx, clusterLatencyTimeout)
raw, errReport := command.Do(commandCtx, "CLUSTER", "BALANCE", "LATENCY", string(payload)).Text()

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 · Home latency RPC adds a prohibited post-connect timeout

This 3-second timeout cancels latency reports from an already established Home command client; the probe path repeats the issue at line 265. On a loaded or high-latency Home taking more than three seconds, otherwise valid reports and probes are discarded, leaving stale measurements and potentially incorrect rebalance decisions. These new post-connect timeouts are not among the repository's allowed exceptions. Also reported for this defect: - Remove the post-connect latency-report deadline (internal/home/cluster_latency.go:230-231): The new three-second context deadline applies to CLUSTER BALANCE LATENCY on an established command connection, not credential acquisition. A delayed response is forcibly aborted. AGENTS.md explicitly prohibits post-establishment network timeouts, and Home telemetry is not a listed exception. - Do not deadline established Home telemetry connections (internal/home/cluster_latency.go:230-231): This adds a three-second deadline to a control RPC on the established Home command pool. With ContextTimeoutEnabled, go-redis applies that deadline to network reads and writes, so a delayed response is aborted. AGENTS.md prohibits post-connection network timeouts outside its named exceptions; Home latency reporting is not an exception or credential acquisition.

helps.ObservePluginExecutorStreamUsage(prepared.outputFormat.String(), lineBuffer, &streamUsage)
lineBuffer = 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.

🔴 Critical · Keep stream usage buffering bounded

Deleting if len(lineBuffer) > maxLineBufferSize removes the only visible bound on the usage accumulator. For an auth-backed plugin stream that emits neither a newline nor complete JSON, every forwarded byte remains buffered even while the downstream consumes it. A reproduction retained approximately 17.6 MiB after forwarding 16 MiB; continuing the stream can exhaust memory and terminate the proxy. Also reported for this defect: - Keep stream usage buffering bounded (internal/pluginhost/adapters_executors.go:959): Deleting if len(lineBuffer) > maxLineBufferSize { ... lineBuffer = nil } leaves no bound on an unfinished stream line. For an auth-backed plugin stream forwarding upstream chunks without a newline or complete JSON value, every byte remains buffered even while downstream drains normally. A reproducer retained roughly 9 MB after forwarding 8 MiB; continued input can exhaust the proxy heap. No replacement bound is visible in the host HTTP or stream bridges. - Partial stream frames can grow without bound (internal/pluginhost/adapters_executors.go:959): With usage reporting enabled, a plugin stream that continuously emits bytes without a newline and without forming complete JSON keeps appending to lineBuffer. This hunk deleted if len(lineBuffer) > maxLineBufferSize { ... lineBuffer = nil }, allowing an upstream stream to exhaust host memory and crash the proxy.

stopCancel := context.AfterFunc(ctx, func() { _ = probe.Close() })
defer func() {
stopCancel()
_ = probe.Close()

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 · Handle the deferred probe-client close error

The probe cleanup discards redis.Client.Close errors, so a transport cleanup failure is neither returned nor logged. AGENTS.md requires checking and logging errors from deferred cleanup; handling errors from the PING commands does not cover this operation.

Comment on lines +128 to +131
if errMkdir := os.MkdirAll(dir, 0o700); errMkdir != nil {
return "", errMkdir
}
return dir, 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.

🔴 Critical · Do not trust a pre-existing shared shadow directory

On Unix with shared /tmp, another user can pre-create /tmp/cliproxy-pluginhost and the predictable PID directory, including symlinks into attacker-writable storage. MkdirAll(..., 0700) neither changes existing permissions nor validates ownership. The attacker can then replace a shadow library after its checksum is read but before dlopen, executing code as the proxy user. The new Unix shadow-loading path needs a genuinely private directory. Also reported for this defect: - Protect the Unix shadow directory from other users (internal/pluginhost/shadow.go:128-131): On Linux with a shared /tmp, another user can pre-create cliproxy-pluginhost with writable permissions or supply a symlink for the predictable process directory. MkdirAll accepts existing directories without validating ownership or permissions. That user can replace the shadow image after copying or hashing but before dlopen, causing arbitrary code execution as the proxy user. This is newly reachable through the Unix loader.

Comment on lines +112 to 122
loadPath, errShadow := shadowCopyPlugin(file)
if errShadow != nil {
return nil, errShadow
}
cPath := C.CString(loadPath)
defer C.free(unsafe.Pointer(cPath))

handle := C.cliproxy_dlopen(cPath)
if handle == nil {
removeShadowPlugin(loadPath)
return nil, fmt.Errorf("dlopen %s: %s", file.Path, dlerrorString())

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 · Unix plugin loading now depends on an executable, writable os.TempDir

Open() no longer dlopens file.Path. It dlopens a copy that shadowCopyPlugin writes under os.TempDir()/cliproxy-pluginhost/pid- (see shadowPluginDir in shadow.go), and nothing falls back to the original path. Two common hardened setups now break plugin loading: - Temp dir mounted noexec, as on CIS-hardened /tmp or docker run --read-only --tmpfs /tmp, whose tmpfs defaults to noexec. dlopen of the copy fails with 'failed to map segment from shared object: Operation not permitted'. - Read-only root filesystem with no writable /tmp. MkdirAll or CreateTemp fails before dlopen is reached. In both cases every configured plugin fails to load (frontend auth, executors, interceptors), even though the same plugin files loaded from the plugin directory before this change.

Comment thread internal/cmd/siwc_bind.go
diskMap["use_siwc"] = true
diskMap["siwc"] = siwcData.ToMap()

return codex.WriteJSONFileAtomic(authFilePath, diskMap)

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 · Persist SIWC binding to the selected storage backend

With PGSTORE_DSN or OBJECTSTORE configured, main supplies the backend's local mirror as AuthDir, but this save updates only that mirror. No backend Save or PersistAuthFiles call follows before reporting success and exiting. On the next startup, Bootstrap replaces the file from the unchanged remote record, losing the newly acquired SIWC credentials.

Comment on lines +112 to +116
loadPath, errShadow := shadowCopyPlugin(file)
if errShadow != nil {
return nil, errShadow
}
cPath := C.CString(loadPath)

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 · Preserve origin-relative native library dependencies

Loading only the copied plugin from the temporary directory changes native dependency resolution. A plugin linked against a sibling library using @loader_path/libhelper.dylib now fails with Library not loaded; the original plugin loads successfully. This was reproduced on Darwin, and $ORIGIN dependencies have the equivalent problem on Linux. The previous Unix loader opened the original artifact location. Also reported for this defect: - Preserve relative native-library dependency resolution (internal/pluginhost/loader_unix.go:116): Loading the copied image changes the directory used by @loader_path dependencies on macOS and $ORIGIN dependencies on Linux, but only the primary library is copied. A plugin linked to an adjacent library therefore fails during dlopen despite loading successfully from its original location. This was reproduced with a dylib referencing @loader_path/libreview_dep.dylib.

Also affects:

return false
}
defer func() {
_ = file.Close()

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 · Handle the deferred verification-file close error

shadowPluginMatches silently discards a Close failure after verifying an existing shadow image. This independently violates AGENTS.md's mandatory deferred-error handling rule and suppresses the cleanup diagnostic even when verification reports success.

Comment on lines +429 to +430
baseModel = strings.TrimSpace(parsed.ModelName)
modelInfo = a.lookupPluginModelInfo(baseModel)

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 · Suffix parsing can produce inconsistent plugin model IDs

For a plugin-routed model not present in the registry, a recognized thinking suffix is stripped into baseModel here even though hasPluginThinkingTarget remains false. Cross-format translation then uses the suffix-free model while nativeReq.Model remains the original suffixed value, so the plugin can receive conflicting model identifiers and reject the request or execute the wrong model.

Comment thread internal/home/client.go
defer func() {
if errClose := conn.Close(); errClose != nil {
log.WithError(errClose).Debug("Home auth dispatch connection close failed")
log.Debugf("Home auth dispatch connection close failed: %v", errClose)

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 · Preserve structured diagnostic fields

Replacing WithError(...).Debug(...) with Debugf removes the structured error field from logrus entries, contrary to AGENTS.md. The same regression occurs at client.go:1972,2245,2257. New formatted diagnostics in siwc_bind.go:151,157,169,174,179,190,197,204,215, cluster_balance.go:79,201,216, and cluster_latency.go:196 likewise place error or plan metadata only in message text. Preserve dynamic diagnostic values as structured fields.

@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-11 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