Skip to content

🤖 fix: allow read-only admin policy in the VS Code webview, redacting gateway URLs - #4807

Merged
ThomasK33 merged 1 commit into
mainfrom
fix/webview-policy
Sep 27, 2026
Merged

ThomasK33 merged 1 commit into
mainfrom
fix/webview-policy

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

The VS Code webview's model list now follows the admin policy (#4739). policy.get and policy.onChanged are added to the webview's oRPC allowlist. The extension host strips each provider's forcedBaseUrl from policy.get before the result reaches the webview.

Background

The webview mounts PolicyProvider (since #4740), but the bridge rejected policy.*, 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.get and policy.onChanged. Nothing else under policy is allowed; a test covers this.

What they return (PolicyGetResponseSchema): source (none, env or governor), status{state, reason?}, and policy{policyFormatVersion, serverVersion?, minimumClientVersion?, providerAccess[{id, allowedModels, forcedBaseUrl?}], mcp.allowUserDefined{stdio, remote}, runtimes}. onChanged yields empty change signals only. Both read a cached in-memory snapshot, with no side effects.

Redactor (redactWebviewOrpcResult in vscode/src/orpcAllowlist.ts, applied in the host's handleOrpcCall to non-stream results):

  • For policy.get it removes forcedBaseUrl from every providerAccess entry. It is an internal gateway URL that could embed credentials, and no browser code reads it; the only other reference is a Storybook story.
  • It returns a copy and leaves every other result unchanged.

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 handleOrpcCall is covered by typecheck. The served-bundle harness below plays the host's side.

Validation

  • Tests written first in vscode/src/orpcAllowlist.test.ts:
    • policy.get and policy.onChanged are allowed; policy.refresh is not.
    • The redactor strips forcedBaseUrl, including a credential-bearing URL, keeps all other fields, and does not mutate its input.
    • A policy-less response and other procedures pass through unchanged.
  • Before the fix: the test file failed to load because redactWebviewOrpcResult did not exist, and both policy paths returned false from isAllowedOrpcPath.
  • bun test ./vscode/src/ passes, 39/39; make static-check passes.
  • I did not add an App-level test for the filtered list: in the unit harness the test plays the host and would answer policy.get itself, so it would pass without this change. The served-bundle check below shows the behavior instead.
Pre-fix output
SyntaxError: Export named 'redactWebviewOrpcResult' not found in module '/home/coder/.mux/src/mux/workspace-5/vscode/src/orpcAllowlist.ts'.
 0 pass
 1 fail
pre-fix isAllowedOrpcPath([policy,get]) = false [policy,onChanged] = false

Dogfood: served bundle

The harness stands in for the host. It answers policy.get with an enforced policy that allows only Anthropic claude-opus-5-5 and claude-sonnet-5, already redacted. The webview requests both paths:

post {"type":"orpcCall","requestId":"4ebbcf2d-79de-4231-b696-dd8ad91bb7a3","path":["policy","get"]}
post {"type":"orpcCall","requestId":"92b610de-99f8-4600-8ac4-86248da0518f","path":["policy","onChanged"]}

Model dropdown before this change, on main, 800px (every suggested model):

Model dropdown before, 800px

With the policy applied, 800px (only Opus 5.5 and Sonnet 5):

Model dropdown with policy, 800px

With the policy applied, 390px:

Model dropdown with policy, 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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T01:43:55.027734Z 2705275 PR opened
🔒 Security Review ✅ Completed 2026-09-27T01:42:56.874509Z 2705275 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread vscode/src/orpcAllowlist.ts
Comment thread vscode/src/orpcAllowlist.ts
@ThomasK33
ThomasK33 added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 9b1f7f9 Sep 27, 2026
57 of 60 checks passed
@ThomasK33
ThomasK33 deleted the fix/webview-policy branch September 27, 2026 02:01
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:

![Policy fallback status line,
800px](https://github.com/user-attachments/assets/6c8ec8ba-571d-4962-8ed7-945d6787d00c)

Fallback, 390px:

![Policy fallback status line,
390px](https://github.com/user-attachments/assets/7aa01b27-6637-4d41-86b7-a1f9196ea0d3)

No allowed listed model, 800px:

![No allowed listed model,
800px](https://github.com/user-attachments/assets/df3f8b44-8465-44ab-a066-3ebd8f1da413)

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

🤖 fix: VS Code webview model list ignores admin policy

1 participant