fix(agents): strip images on cross-provider fallback retry#43485
fix(agents): strip images on cross-provider fallback retry#43485mitchmcalister wants to merge 12 commits intoopenclaw:mainfrom
Conversation
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟠 Cross-provider model fallback resends full original prompt, causing unintended third-party disclosure
DescriptionIn This creates a privacy regression when the fallback selects a different provider (cross-provider failover):
Previously, fallback retries used a generic continuation string ("Continue where you left off...") which reduced disclosure of the full original prompt to a different provider. Vulnerable code (cross-provider fallback will still receive the full prompt): // ...
const effectivePrompt = params.body;
// ...
return runEmbeddedPiAgent({
// ...
prompt: effectivePrompt,
provider: params.providerOverride,
model: params.modelOverride,
// ...
});Even though images are conditionally stripped on cross-provider fallback via RecommendationPreserve the reduced-disclosure behavior for cross-provider fallback retries, or require explicit opt-in. Suggested fix: only resend the original prompt on fallback when the provider is unchanged; otherwise send a generic continuation prompt (or abort unless the user explicitly allows cross-provider disclosure). Example: const isCrossProvider =
normalizeProviderId(params.primaryProvider) !== normalizeProviderId(params.providerOverride);
const effectivePrompt =
params.isFallbackRetry && isCrossProvider
? "Continue where you left off. The previous model attempt failed or timed out."
: params.body;Additionally:
2. 🟡 Model fallback retry re-sends original prompt, risking duplicate side-effectful tool execution
DescriptionIn This creates a replay risk when a fallback retry happens after the first attempt already executed tools or produced assistant output, but then throws a Why this is security-relevant:
The code comment claims orphaned-user-message repair prevents duplicates, but that repair only removes a trailing orphaned user message when the last persisted entry is Vulnerable change (replay trigger): // Fallback retries only fire on thrown errors...
const effectivePrompt = params.body;Relevant supporting behavior:
RecommendationAdd replay/idempotency protection for fallback retries, because a thrown error can occur after tools have already executed. Options (pick one or combine):
Example direction (pseudo): // before executing a side-effectful tool
const key = `${runId}:${toolName}:${stableArgsHash}`;
if (ledger.has(key)) return ledger.get(key); // return cached result
ledger.set(key, result);
Analyzed PR: #43485 at commit Last updated on: 2026-03-11T22:43:56Z |
Greptile SummaryThis PR addresses a security gap where user-supplied images were being forwarded to third-party providers during cross-provider fallback retries. It also fixes a related UX regression where fallback retries were replacing the user's original prompt with a generic "Continue where you left off" string. Key changes:
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/commands/agent.test.ts
Line: 1041-1048
Comment:
**Missing test: cross-provider strip when `previousFailureReason` is `undefined`**
The existing unit tests confirm that cross-provider stripping fires when a `previousFailureReason` is set (e.g., `"rate_limit"`, `"timeout"`). However, there is no test for the case where providers differ but `previousFailureReason` is `undefined`. Reading `resolveRetryImages`, the provider check runs first and is independent of `previousFailureReason`, so the function would still return `undefined`; but adding an explicit case makes the independence explicit and guards against a future refactor that accidentally gates the provider check on `previousFailureReason` being set.
```ts
it("strips images on cross-provider fallback even when previousFailureReason is undefined", () => {
expect(
resolveRetryImages(fakeImages, true, undefined, "anthropic", "openai"),
).toBeUndefined();
});
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/commands/agent.test.ts
Line: 597-651
Comment:
**Integration test doesn't verify image stripping on cross-provider retry**
The test exercises the cross-provider retry path (`anthropic` → `openai/gpt-5.2`) and correctly verifies prompt forwarding, but `agentCommand` is called without any `images`, so there is no integration-level assertion that `resolveRetryImages` is actually wired up and stripping images on this path.
Consider passing a `fakeImages` array in the `agentCommand` call and asserting that the first call receives the original images while the second call receives `undefined`. The unit tests for `resolveRetryImages` cover the pure-function behaviour thoroughly, but this integration path (`agentCommand` → `runAgentAttempt` → `runEmbeddedPiAgent`) is the security-critical one and currently has no assertion to confirm the plumbing works end-to-end.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: a608220 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a6082200e6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Addressing Aisle Security FindingsFinding 1 (High) — Cross-provider prompt forwardingThis is an architectural property of the fallback system, not a regression from this PR. Cross-provider fallbacks exist so that when the primary provider fails, the retry can succeed on a different provider — which inherently requires sending the prompt. If operators configure cross-provider fallbacks (e.g. Stripping or replacing the prompt on cross-provider retry (as suggested) would make the retry non-functional — the fallback model would have no context to work with. If there's appetite for a config gate like Finding 2 (Medium) — Prompt-detected images bypassAlready tracked in #43492, filed from review feedback on this PR. The |
|
Converting to draft — this will be superseded by a unified approach that addresses the cross-provider image privacy concern at the session level rather than patching See #43331 (comment) for context on the rethink. |
Delete no-op resolveFallbackRetryPrompt (inline body directly), simplify resolveRetryImages to positional args with a single guard, and replace the effectiveOptions IIFE with a conditional spread one-liner. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a fallback retry targets a different provider than the primary, strip user-supplied images to avoid unintended third-party disclosure. Closes openclaw#43481
Adds unit test for cross-provider strip with undefined failureReason, and integration test verifying images are plumbed through agentCommand and stripped on cross-provider fallback retry end-to-end.
…nstead of user-turn text
997ad4a to
8491415
Compare
|
Superseded by #44188 (unified fallback retry safety PR). |
Summary
Closes #43481
Related: #43331 (parent behavioral fix that surfaced this during security review)
Test plan
pnpm test -- src/commands/agent.test.ts— allresolveRetryImagestests pass (4 new + 6 existing)pnpm format— clean🤖 Generated with Claude Code