🤖 fix: allow read-only admin policy in the VS Code webview, redacting gateway URLs - #4807
Merged
Merged
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 27052754d6
ℹ️ 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".
This was referenced Sep 27, 2026
Closed
github-merge-queue Bot
pushed a commit
that referenced
this pull request
Sep 27, 2026
…S Code webview's selection (#4811) ## Summary When the enforced admin policy excludes the VS Code webview's selected model, the composer now uses the first allowed model from its list for display and send, and a status line says which model the policy rejected and which one is used. The stored choice is never overwritten. If no listed model is allowed, the composer keeps the stored model and the status line asks the user to choose an allowed one. Before, every send failed with `policy_denied` with no hint until the user picked another model. ## Background The selected model can be one the policy disallows: persisted earlier, seeded from the workspace (#4778), or revoked by a policy refresh. Since #4807 the dropdown hides disallowed models, but the composer still showed and sent the stored one. Found in review of #4807. The desktop composer has no such reconciliation to reuse (it only filters the model lists), so this is webview-local. ## Implementation `vscode/src/webview/ChatComposer.tsx` (webview only): - Checks the stored model with the route-aware `isAllowedByPolicyOnActiveRoute` from the shared `useModelsFromSettings` hook, the same predicate the model list uses, because the backend enforces policy after routing. The check uses the gateway-preserving identity (`normalizeSelectedModel`), so an explicitly pinned gateway model is checked on that gateway. - If excluded, picks the first entry of the (already policy-filtered) `models` list that passes the same check. That model drives the selector, the thinking control, model cycling, and the send's `model` option. The send overrides `model` only in this case, so the normal send keeps the stored model string as before. - Nothing is written: no `localStorage` change and no `updateAgentAISettings` call, because webview persistence is off (#4755/#4781). If the policy later allows the stored model again, it comes back by itself. - No allowed listed model (for example, the policy only allows an unlisted custom model): nothing else changes; the status line says so and the backend decides, as before. - The status line is a `role="status"` text line above the model selector, using the `text-content-secondary` token. Review history: round 1 asked for the route-aware check and for `/vim` to keep working in a "Send disabled" state; round 2 found the check dropped explicit gateway IDs and the disabled button still blocked `/vim` by mouse. Both rounds clustered on the blocking state, so it was removed (scope reduction) instead of adding more special cases. ## Validation - Test-first in `App.test.tsx` (TestBridge now has an `answer(path, value)` helper that plays the host's `orpcResponse`), with `policy.get` answered with an enforced policy and the stored model `anthropic:claude-opus-5-5`: 1. Policy allows only `openai:gpt-5.6-terra`: the status line names the rejected model, the send carries `model: "openai:gpt-5.6-terra"`, the stored model is unchanged, and no `updateAgentAISettings` is posted. On `main`: no status line, and the send carries `anthropic:claude-opus-5-5`. 2. Policy allows only an unlisted model: status line shown and the send carries the stored model. On `main`: no status line. 3. Policy allows the stored model: no status line and the send is unchanged (guards the normal path). - A gateway-pinned regression test is not possible in the webview yet: it resolves routes without the providers config until #4766 connects it (PR in flight), so an explicit gateway currently falls back to the direct route there. - Served-bundle dogfood (#4740 harness, `policy.get` answered with an enforced policy allowing `openai:gpt-5.6-terra` and `gpt-5.6-sol`, stored model Opus 5.5): - The composer showed "Admin policy does not allow anthropic:claude-opus-5-5; using openai:gpt-5.6-terra." with GPT-5.6 Terra selected. - Send posted `workspace.sendMessage` with `model: "openai:gpt-5.6-terra"` and `skipAiSettingsPersistence: true`; no `updateAgentAISettings` post; `localStorage["model:ws-demo"]` stayed `"anthropic:claude-opus-5-5"`. 0 console errors. - With a policy that allows only an unlisted model: "Admin policy does not allow anthropic:claude-opus-5-5. Choose an allowed model." and Send stays enabled. Fallback, 800px:  Fallback, 390px:  No allowed listed model, 800px:  ## Risks Low, webview only. The webview does not load routing preferences or the providers config until #4766, so today the active route is the default one and "first allowed model" means the first allowed built-in model. Fixes #4808 --- _Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high` • Cost: `$4.48`_ <!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high costs=4.48 -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The VS Code webview's model list now follows the admin policy (#4739).
policy.getandpolicy.onChangedare added to the webview's oRPC allowlist. The extension host strips each provider'sforcedBaseUrlfrompolicy.getbefore the result reaches the webview.Background
The webview mounts
PolicyProvider(since #4740), but the bridge rejectedpolicy.*, so the provider fell back to "no policy". The composer's model dropdown therefore offered models the admin had disallowed; it lists every suggested model when the providers config is not loaded. The backend still rejected those sends (policy_denied), so this was a UI mismatch, not a bypass.Security: what this adds (the webview is treated as less trusted than the extension host)
Allowlist paths added:
policy.getandpolicy.onChanged. Nothing else underpolicyis allowed; a test covers this.What they return (
PolicyGetResponseSchema):source(none,envorgovernor),status{state, reason?}, andpolicy{policyFormatVersion, serverVersion?, minimumClientVersion?, providerAccess[{id, allowedModels, forcedBaseUrl?}], mcp.allowUserDefined{stdio, remote}, runtimes}.onChangedyields empty change signals only. Both read a cached in-memory snapshot, with no side effects.Redactor (
redactWebviewOrpcResultinvscode/src/orpcAllowlist.ts, applied in the host'shandleOrpcCallto non-stream results):policy.getit removesforcedBaseUrlfrom everyproviderAccessentry. It is an internal gateway URL that could embed credentials, and no browser code reads it; the only other reference is a Storybook story.What remains visible: provider IDs, allowed model lists, runtime and MCP flags, and the status reason. That is admin configuration with no secrets, and it only keeps the webview's model list consistent with what the backend already enforces.
Not exercised end to end: the redactor runs in the extension host, and I had no VS Code host to run it in. It is a pure function covered by unit tests, and its wiring into
handleOrpcCallis covered by typecheck. The served-bundle harness below plays the host's side.Validation
vscode/src/orpcAllowlist.test.ts:policy.getandpolicy.onChangedare allowed;policy.refreshis not.forcedBaseUrl, including a credential-bearing URL, keeps all other fields, and does not mutate its input.redactWebviewOrpcResultdid not exist, and both policy paths returnedfalsefromisAllowedOrpcPath.bun test ./vscode/src/passes, 39/39;make static-checkpasses.policy.getitself, so it would pass without this change. The served-bundle check below shows the behavior instead.Pre-fix output
Dogfood: served bundle
The harness stands in for the host. It answers
policy.getwith an enforced policy that allows only Anthropicclaude-opus-5-5andclaude-sonnet-5, already redacted. The webview requests both paths:Model dropdown before this change, on
main, 800px (every suggested model):With the policy applied, 800px (only Opus 5.5 and Sonnet 5):
With the policy applied, 390px:
The console showed 0 errors.
Risks
Low. Two read-only procedures are added, and the one URL field is redacted. If a future policy field is sensitive, the redactor must be extended; the comments at the allowlist entry and in the redactor point there.
Fixes #4739
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:high• Cost:$43.37