Skip to content

sessions: show the approvals picker for agent hosts without an approvals config property - #333926

Draft
Osvaldo Ortega (osortega) wants to merge 3 commits into
mainfrom
osortega/agents/permissions-picker-comparison
Draft

sessions: show the approvals picker for agent hosts without an approvals config property#333926
Osvaldo Ortega (osortega) wants to merge 3 commits into
mainfrom
osortega/agents/permissions-picker-comparison

Conversation

@osortega

@osortega Osvaldo Ortega (osortega) commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The problem

The session-level approvals picker only renders when the session's config schema declares a well-known autoApprove string enum. Any agent host that drives approvals a different way gets no picker at all — the chip silently never appears.

That is what this looks like today: the mode chip renders, and the approvals chip that should sit beside it is simply absent. There is no error and no fallback, so the session runs on whatever approval behavior the host defaults to and the user has no way to see or change it.

What this changes

Adds a fallback path for hosts with no approvals property in their session config.

The picker is backed by that request when three things hold: the session runs the Copilot agent, the connection can issue the request, and the resolved schema declares no approvals property. Because the request carries a boolean, the picker then offers the two levels a boolean can express:

  • Manual permissions — ask before running tools
  • Allow all — run tools without asking

Everything else is untouched. Hosts that declare an approvals property keep the existing config path and their full three-level set including Assisted; agents with their own approvals axis (Claude, Codex) keep their dedicated pickers; and the in-process CLI session, which shares the Copilot agent name but whose connection cannot issue the request, also stays on the config path. Gating on the agent positively means an agent this client has never heard of gets no picker rather than the wrong one. Tests cover each of these.

Why the extra state handling

The extension's resulting value is not reported back in session state, so a client that wants to display it has to remember what it applied. Three consequences, each guarded:

Only confirmed values are recorded. The record is written after the host accepts, never optimistically. A rejected call leaves the picker showing what is actually in effect rather than what was requested. This fails in the safe direction — a dropped enable means the user keeps being asked.

The value is re-applied when a session's state subscription is established. A host that restarts, or a remote environment that suspends and resumes, comes back with the flag cleared while the local record still says otherwise. Re-applying on subscribe keeps the two in step.

Two ordering hazards are guarded explicitly:

  • The record is read-modify-write against storage rather than cached in the provider. Every agent-host provider in the window shares one profile-scoped key, so a cached copy would let the last writer drop the other providers' entries. Session ids are provider-qualified, so entries cannot collide.
  • A toggle already on the wire suppresses the re-apply. Without that, a subscription established mid-flight could re-send a stale true after the user's false, leaving a session auto-approving right after they turned it off.

Reviewing

The load-bearing code is the set of guards in _usesApproveAll. The !config check matters because an unresolved schema must not be mistaken for "this host has no approvals property", or the picker would briefly offer two levels to a host that turns out to offer three. The agent check matters because an agent that omits autoApprove may be doing so deliberately to suppress the default UI, which is not the same as having no approvals axis.

canRequestSessionApproveAll() is named for what it checks. It confirms the connection implements the optional method; it does not prove the host handles it, because this vendor extension is not advertised in the host's initialize capabilities the way our own extensions are gated. Until it is, the schema check is the real discriminator. A host with neither would surface a picker whose writes fail — again in the safe direction.

Tests

15 new tests. Notably: the value the host confirmed vs. the one requested, the host-rejects path, surviving a reload and being re-applied on resubscribe, one provider not dropping another's record, hosts that declare an approvals property staying on the existing path, agents with their own approvals axis being left alone, and the agent name alone not being sufficient to claim a session.

Verified: typecheck-client, valid-layers-check, hygiene, vs/sessions (2915 passing), AgentHost (2694 passing).

Not verified: I have not exercised this against a live remote host — no observation of the request succeeding over a real connection, of the re-apply firing after a genuine suspend/resume, or of the rendered two-level picker. Worth a manual pass before this leaves draft.

Known limits

  • Two levels, not three. Assisted has no representation in a boolean.
  • No cross-client sync. The record reflects what this client applied. Another client changing the same session leaves it stale. The durable fix is the host reporting the value in session state, which would let the local persistence and the re-apply be deleted outright — the storage constant documents exactly what to remove when that lands.
  • No picker in the new-chat view for sessions whose host does not exist yet. Deliberate: with no host to ask, the available options cannot be discovered, and showing a guessed list would be worse than showing nothing.

…als config property

The session-level approvals picker requires the session's config schema to
declare a well-known `autoApprove` string enum. A host that drives approvals
some other way therefore had no picker at all — the chip silently never
rendered, even though a mode chip beside it did.

Add a fallback for those hosts. When the resolved schema declares no approvals
property but the connection can issue the Copilot approve-all extension, back
the picker with that instead and offer the two levels a boolean can express
(Manual permissions / Allow all). Hosts that do declare an approvals property
are untouched and keep their full level set.

The extension's value is not reported back in session state, so the applied
value is recorded locally and re-applied when a session's state subscription is
established — a host that restarts or resumes comes back with it cleared. Only
values the host confirms are recorded, so a rejected call leaves the picker
showing what is actually in effect rather than what was requested. Failing to
widen approvals is the safe direction: the user keeps being asked.

Two ordering hazards are guarded explicitly. The persisted record is
read-modify-write against storage rather than cached, because every agent-host
provider in the window shares one profile-scoped key and a cached copy would
let the last writer drop the others' entries. And a toggle already on the wire
suppresses the re-apply, so a subscription established mid-flight cannot
re-send a stale `true` after the user has just turned approvals off.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings September 2, 2026 01:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Local forwarding, schema selection, lifecycle restoration, policy handling, rollback, and request ordering contain unresolved defects.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 2 High severity · 3 Medium severity

New issues introduced by this change (5)
Severity Finding
High severity src/​vs/​sessions/​contrib/​providers/​agentHost/​browser/​baseAgentHostSessionsProvider.ts — The set tracks only membership; it neither serializes multiple quick user selections nor includes…
High severity src/​vs/​sessions/​contrib/​providers/​agentHost/​browser/​baseAgentHostSessionsProvider.ts — A remembered true is replayed without checking whether enterprise policy now forbids elevated…
Medium severity src/​vs/​platform/​agentHost/​common/​agentService.ts — Because this method is optional, LocalAgentHostServiceClient still satisfies IAgentHostService
Medium severity src/​vs/​sessions/​contrib/​providers/​agentHost/​browser/​agentHostPermissionPickerDelegate.tsPermissionPicker._selectLevel updates its trigger optimistically before invoking this void…
Medium severity src/​vs/​sessions/​contrib/​providers/​agentHost/​browser/​baseAgentHostSessionsProvider.ts — This executes when the provider acquires its local reference, not when the wire subscription is…
What changed in this PR

Adds a two-level approval picker fallback for Agent Hosts without an approvals configuration property.

Changes:

  • Adds the approve-all extension request and provider APIs.
  • Persists and reapplies confirmed approve-all selections.
  • Adds fallback, persistence, rejection, and provider-isolation tests.
File Description
src/​vs/​platform/​agentHost/​common/​agentService.ts Extends the connection contract.
src/​vs/​platform/​agentHost/​common/​agentHostExtensionProtocol.ts Defines the extension request.
src/​vs/​platform/​agentHost/​browser/​agentHostProtocolClient.ts Sends approve-all requests.
src/​vs/​sessions/​common/​agentHostSessionsProvider.ts Exposes provider approval operations.
src/​vs/​sessions/​contrib/​providers/​agentHost/​browser/​baseAgentHostSessionsProvider.ts Persists and reapplies approval state.
src/​vs/​sessions/​contrib/​providers/​agentHost/​browser/​agentHostPermissionPickerDelegate.ts Selects and drives the fallback picker.
src/​vs/​sessions/​contrib/​providers/​agentHost/​test/​browser/​localAgentHostSessionsProvider.test.ts Tests persistence and provider isolation.
src/​vs/​sessions/​contrib/​providers/​agentHost/​test/​browser/​agentHost/​agentHostPermissionPickerDelegate.test.ts Tests fallback picker behavior.
Suppressed comments (1)

src/vs/sessions/contrib/providers/agentHost/browser/agentHostPermissionPickerDelegate.ts:280

  • This also selects the fallback when an autoApprove property exists but has a noncanonical schema. That property still renders through the generic config picker (agentHostSessionConfigPicker.ts:499-503), so such hosts get two approval pickers backed by different mechanisms. The fallback is specifically for an absent property; leave every declared property on the existing config path.
		return !schema || !isWellKnownAutoApproveSchema(schema);

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +3855 to +3859
this._approveAllInFlight.add(sessionId);
try {
await connection.setSessionApproveAll(cached.backendUri, enabled);
} finally {
this._approveAllInFlight.delete(sessionId);
Comment on lines +3878 to +3882
if (!connection.setSessionApproveAll || this._approveAllInFlight.has(sessionId) || !this.getSessionApproveAll(sessionId)) {
return;
}
connection.setSessionApproveAll(sessionUri, true).catch(err => {
this._logService.warn(`[${this.id}] Failed to re-apply approve-all for ${sessionId}: ${err}`);
* method — a connection that cannot serve it simply omits it, and callers
* fall back to the session-config approvals path.
*/
setSessionApproveAll?(session: URI, enabled: boolean): Promise<void>;
Comment on lines +198 to +199
provider.setSessionApproveAll(session.sessionId, level === ChatPermissionLevel.AutoApprove)
.catch(err => this._logService.warn(`[AgentHostPermissionPicker] Failed to set approve-all: ${err}`));

this._hydrateAgentFromDraft(connection, cached, sessionId, sessionUri, store);
this._hydrateModelFromDraft(connection, cached, sessionId, sessionUri, store);
this._reassertSessionApproveAll(connection, sessionId, sessionUri);
The fallback keyed only off the absence of a well-known `autoApprove`
property. Claude omits that property deliberately — its schema comment notes
that workbench pickers key off property names, so leaving it out suppresses the
default UI — which meant a remote Claude or Codex session was read as "no
approvals axis" and claimed by the fallback. That stacked a second approvals
chip beside the agent's own picker and would have sent a Copilot-shaped request
to an agent that never asked for one.

Gate on the Copilot agent instead. The request is a Copilot host extension, so
that is exactly the set of sessions where it means anything, and gating
positively fails closed: an agent this client has never heard of now gets no
picker rather than the wrong one.

The connection and schema checks still apply on top, so the in-process CLI
session (which shares the `copilotcli` agent name but whose connection cannot
issue the request) and Copilot on hosts that do declare approvals both stay on
the config path with their full level set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ll writes

Fixes the approvals picker not appearing on a real cloud sandbox session. The
agent gate only accepted `copilotcli`, the provider id used by the agent host
bundled with VS Code. The Copilot host advertises its agent as `copilot`, so a
sandbox session type (`remote-<authority>-copilot`) failed the gate and the
picker stayed hidden. Accept both ids, and pin an observed sandbox session type
in a test so the two spellings cannot drift apart again.

Also addresses three review findings on the write path:

Enterprise policy is now enforced here as it already is on the session-config
path. Enabling approve-all is refused outright under a restricting policy, and
a value remembered from an earlier run is dropped rather than replayed when
policy has since turned elevated approvals off — previously the replay was a
way around a restriction the rest of the UI honours.

Writes for a session are serialized and only the newest one's result is kept,
so toggling quickly settles on what the user asked for last rather than on
whichever request happened to resolve last. Previously two in-flight writes
could resolve out of order, and an early completion cleared the in-flight
marker while another write was still pending.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

2 participants