Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { AgentSession, IAgentConnection, IAgentCreateSessionConfig, IAgentResolv
import { AgentSubscriptionManager, type IAgentSubscription } from '../common/state/agentSubscription.js';
import { agentHostAuthority, fromAgentHostUri, toAgentHostUri } from '../common/agentHostUri.js';
import type { ClientNotificationMap, CommandMap, JsonRpcErrorResponse, JsonRpcRequest } from '../common/state/protocol/messages.js';
import type { ActionEnvelope, INotification, SessionAction, TerminalAction } from '../common/state/sessionActions.js';
import type { ActionEnvelope, INotification, RootAction, SessionAction, TerminalAction } from '../common/state/sessionActions.js';
import { SessionSummary, SessionStatus, ROOT_STATE_URI, StateComponents, type RootState } from '../common/state/sessionState.js';
import { PROTOCOL_VERSION } from '../common/state/sessionCapabilities.js';
import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, type ProtocolMessage, type IStateSnapshot } from '../common/state/sessionProtocol.js';
Expand Down Expand Up @@ -182,7 +182,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
return this._subscriptionManager.getSubscriptionUnmanaged<T>(resource);
}

dispatch(action: SessionAction | TerminalAction): void {
dispatch(action: RootAction | SessionAction | TerminalAction): void {
const seq = this._subscriptionManager.dispatchOptimistic(action);
this.dispatchAction(action, this._clientId, seq);
}
Expand All @@ -205,7 +205,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
/**
* Dispatch a client action to the server. Returns the clientSeq used.
*/
dispatchAction(action: SessionAction | TerminalAction, _clientId: string, clientSeq: number): void {
dispatchAction(action: RootAction | SessionAction | TerminalAction, _clientId: string, clientSeq: number): void {
this._sendNotification('dispatchAction', { clientSeq, action });
}

Expand Down
110 changes: 71 additions & 39 deletions src/vs/platform/agentHost/common/agentHostSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,29 @@ export interface ISchema<D extends SchemaDefinition> {
*/
assertValid<K extends keyof D & string>(key: K, value: unknown): asserts value is SchemaValue<D[K]>;
/**
* Returns a fully-typed values bag by validating each key of
* `defaults` against `values` and falling back to the default when
* Returns a fully-typed values bag by validating each key of the
* schema against `values` and falling back to the default when
* the incoming value is missing or fails validation.
*
Comment thread
connor4312 marked this conversation as resolved.
* Semantics: for every key declared in the schema `definition`:
* - if `values[key]` validates, it is kept;
* - else if `key` is present in `defaults`, the default is used;
* - else the key is omitted from the result.
*
* This means callers MAY supply defaults for only a subset of the
* schema — keys not present in `defaults` are simply left unset
* when the incoming value is missing or invalid. This is useful
* when some properties (e.g. per-session `permissions`) should be
* inherited from a higher scope rather than materialized on every
* new session.
*
* Intended for sanitizing untrusted input at protocol boundaries
* (e.g. `resolveSessionConfig`), where callers want a complete
* type-safe object rather than a throw-on-first-error response.
* Keys that fail validation are silently replaced with their
* default; use {@link values} or {@link assertValid} when you want
* a descriptive {@link ProtocolError} instead.
* (e.g. `resolveSessionConfig`). Keys that fail validation are
* silently replaced with their default or dropped; use
* {@link values} or {@link assertValid} when you want a descriptive
* {@link ProtocolError} instead.
*/
validateOrDefault<T extends { [K in keyof D]: SchemaValue<D[K]> }>(values: Record<string, unknown> | undefined, defaults: T): T;
validateOrDefault<T extends Partial<{ [K in keyof D]: SchemaValue<D[K]> }>>(values: { [K in keyof T]?: unknown } | undefined, defaults: T): T;
}

export function createSchema<D extends SchemaDefinition>(definition: D): ISchema<D> {
Expand Down Expand Up @@ -147,14 +158,19 @@ export function createSchema<D extends SchemaDefinition>(definition: D): ISchema
const narrowed: ISchemaProperty<unknown> = prop;
narrowed.assertValid(value, key);
},
validateOrDefault<T extends { [K in keyof D]: SchemaValue<D[K]> }>(values: Record<string, unknown> | undefined, defaults: T): T {
validateOrDefault<T extends Partial<{ [K in keyof D]: SchemaValue<D[K]> }>>(values: { [K in keyof T]?: unknown } | undefined, defaults: T): T {
const result: Record<string, unknown> = {};
for (const key of Object.keys(defaults)) {
const raw = values?.[key];
const raw: { [K in keyof T]?: unknown } = values ?? {};
for (const key of Object.keys(definition)) {
const prop = definition[key];
result[key] = prop && raw !== undefined && prop.validate(raw)
? raw
: (defaults as Record<string, unknown>)[key];
const candidate = raw[key];
if (candidate !== undefined && prop.validate(candidate)) {
result[key] = candidate;
} else if (Object.prototype.hasOwnProperty.call(defaults, key)) {
result[key] = (defaults as Record<string, unknown>)[key];
}
// else: key not in defaults and incoming value missing/invalid
// → leave unset so higher-scope defaults can fill in.
}
return result as T;
},
Expand Down Expand Up @@ -249,6 +265,32 @@ export interface IPermissionsValue {
readonly deny: readonly string[];
}

const permissionsProperty = schemaProperty<IPermissionsValue>({
type: 'object',
title: localize('agentHost.sessionConfig.permissions', "Permissions"),
description: localize('agentHost.sessionConfig.permissionsDescription', "Per-tool session permissions. Updated automatically when approving a tool \"in this Session\"."),
properties: {
allow: {
type: 'array',
title: localize('agentHost.sessionConfig.permissions.allow', "Allowed tools"),
items: {
type: 'string',
title: localize('agentHost.sessionConfig.permissions.toolName', "Tool name"),
},
},
deny: {
type: 'array',
title: localize('agentHost.sessionConfig.permissions.deny', "Denied tools"),
items: {
type: 'string',
title: localize('agentHost.sessionConfig.permissions.toolName', "Tool name"),
},
},
},
default: { allow: [], deny: [] },
sessionMutable: true,
});

/**
* Session-config properties owned by the platform itself — i.e. consumed
* by the agent host rather than by any particular agent.
Expand Down Expand Up @@ -276,29 +318,19 @@ export const platformSessionSchema = createSchema({
default: 'default',
sessionMutable: true,
}),
[SessionConfigKey.Permissions]: schemaProperty<IPermissionsValue>({
type: 'object',
title: localize('agentHost.sessionConfig.permissions', "Permissions"),
description: localize('agentHost.sessionConfig.permissionsDescription', "Per-tool session permissions. Updated automatically when approving a tool \"in this Session\"."),
properties: {
allow: {
type: 'array',
title: localize('agentHost.sessionConfig.permissions.allow', "Allowed tools"),
items: {
type: 'string',
title: localize('agentHost.sessionConfig.permissions.toolName', "Tool name"),
},
},
deny: {
type: 'array',
title: localize('agentHost.sessionConfig.permissions.deny', "Denied tools"),
items: {
type: 'string',
title: localize('agentHost.sessionConfig.permissions.toolName', "Tool name"),
},
},
},
default: { allow: [], deny: [] },
sessionMutable: true,
}),
[SessionConfigKey.Permissions]: permissionsProperty,
});

/**
* Root (agent host) config properties owned by the platform itself.
*
* Root config acts as the baseline that applies to every session:
*
* - {@link SessionConfigKey.Permissions} — host-wide allow/deny lists
* unioned with each session's own permissions when evaluating tool
* auto-approval. See `SessionPermissionManager` for the evaluation
* rules.
*/
export const platformRootSchema = createSchema({
[SessionConfigKey.Permissions]: permissionsProperty,
});
6 changes: 3 additions & 3 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { ISyncedCustomization } from './agentPluginManager.js';
import type { IAgentSubscription } from './state/agentSubscription.js';
import type { CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from './state/protocol/commands.js';
import { ProtectedResourceMetadata, type ConfigSchema, type FileEdit, type ModelSelection, type SessionActiveClient, type ToolDefinition } from './state/protocol/state.js';
import type { ActionEnvelope, INotification, SessionAction, TerminalAction } from './state/sessionActions.js';
import type { ActionEnvelope, INotification, RootAction, SessionAction, TerminalAction } from './state/sessionActions.js';
import type { ResourceCopyParams, ResourceCopyResult, ResourceDeleteParams, ResourceDeleteResult, ResourceListResult, ResourceMoveParams, ResourceMoveResult, ResourceReadResult, ResourceWriteParams, ResourceWriteResult, IStateSnapshot } from './state/sessionProtocol.js';
import { AttachmentType, ComponentToState, SessionInputResponseKind, SessionStatus, StateComponents, type CustomizationRef, type PendingMessage, type RootState, type SessionInputAnswer, type SessionInputRequest, type ToolCallResult, type ToolResultContent, type PolicyState, type StringOrMarkdown } from './state/sessionState.js';

Expand Down Expand Up @@ -615,7 +615,7 @@ export interface IAgentService {
* it to state, triggers side effects, and echoes it back via
* {@link onDidAction} with the client's origin for reconciliation.
*/
dispatchAction(action: SessionAction | TerminalAction, clientId: string, clientSeq: number): void;
dispatchAction(action: RootAction | SessionAction | TerminalAction, clientId: string, clientSeq: number): void;

/**
* List the contents of a directory on the agent host's filesystem.
Expand Down Expand Up @@ -668,7 +668,7 @@ export interface IAgentConnection {
getSubscriptionUnmanaged<T extends StateComponents>(kind: T, resource: URI): IAgentSubscription<ComponentToState[T]> | undefined;

// ---- Action dispatch ----------------------------------------------------
dispatch(action: SessionAction | TerminalAction): void;
dispatch(action: RootAction | SessionAction | TerminalAction): void;

// ---- Events (connection-level) ------------------------------------------
readonly onDidNotification: Event<INotification>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ export class AgentSubscriptionManager extends Disposable {
* Dispatch a client action. Applies optimistically to the relevant
* subscription if applicable, then returns the clientSeq.
*/
dispatchOptimistic(action: SessionAction | TerminalAction): number {
dispatchOptimistic(action: RootAction | SessionAction | TerminalAction): number {
if (isSessionAction(action)) {
const entry = this._subscriptions.get(URI.parse(action.session));
if (entry && entry.sub instanceof SessionStateSubscription) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ILogService } from '../../log/common/log.js';
import { AgentHostEnabledSettingId, AgentHostIpcChannels, IAgentCreateSessionConfig, IAgentHostInspectInfo, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IAgentHostSocketInfo, IConnectionTrackerService } from '../common/agentService.js';
import { AgentSubscriptionManager, type IAgentSubscription } from '../common/state/agentSubscription.js';
import type { CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../common/state/protocol/commands.js';
import type { ActionEnvelope, INotification, SessionAction, TerminalAction } from '../common/state/sessionActions.js';
import type { ActionEnvelope, INotification, RootAction, SessionAction, TerminalAction } from '../common/state/sessionActions.js';
import type { ResourceCopyParams, ResourceCopyResult, ResourceDeleteParams, ResourceDeleteResult, ResourceListResult, ResourceMoveParams, ResourceMoveResult, ResourceReadResult, ResourceWriteParams, ResourceWriteResult, IStateSnapshot } from '../common/state/sessionProtocol.js';
import { StateComponents, ROOT_STATE_URI, type RootState } from '../common/state/sessionState.js';
import { revive } from '../../../base/common/marshalling.js';
Expand Down Expand Up @@ -160,7 +160,7 @@ class AgentHostServiceClient extends Disposable implements IAgentHostService {
unsubscribe(resource: URI): void {
this._proxy.unsubscribe(resource);
}
dispatchAction(action: SessionAction | TerminalAction, clientId: string, clientSeq: number): void {
dispatchAction(action: RootAction | SessionAction | TerminalAction, clientId: string, clientSeq: number): void {
this._proxy.dispatchAction(action, clientId, clientSeq);
}
private _nextSeq = 1;
Expand All @@ -180,7 +180,7 @@ class AgentHostServiceClient extends Disposable implements IAgentHostService {
return this._subscriptionManager.getSubscriptionUnmanaged<T>(resource);
}

dispatch(action: SessionAction | TerminalAction): void {
dispatch(action: RootAction | SessionAction | TerminalAction): void {
const seq = this._subscriptionManager.dispatchOptimistic(action);
this.dispatchAction(action, this.clientId, seq);
}
Expand Down
17 changes: 16 additions & 1 deletion src/vs/platform/agentHost/node/agentHostStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { ActionType, NotificationType, ActionEnvelope, ActionOrigin, INotificati
import type { IStateSnapshot } from '../common/state/sessionProtocol.js';
import { rootReducer, sessionReducer } from '../common/state/sessionReducers.js';
import { createRootState, createSessionState, SessionLifecycle, type RootState, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI } from '../common/state/sessionState.js';
import { IPermissionsValue, platformRootSchema } from '../common/agentHostSchema.js';
import { SessionConfigKey } from '../common/sessionConfigKeys.js';

/**
* Server-side state manager for the sessions process protocol.
Expand Down Expand Up @@ -46,6 +48,19 @@ export class AgentHostStateManager extends Disposable {
) {
super();
this._rootState = createRootState();
// Seed the host-level configuration schema + default values so that
// RootConfigChanged actions can merge into it, and clients see the
// schema immediately upon subscribing to `agenthost:/root`. See
// `platformRootSchema` for the set of platform-owned properties.
this._rootState = {
...this._rootState,
config: {
schema: platformRootSchema.toProtocol(),
values: platformRootSchema.validateOrDefault({}, {
[SessionConfigKey.Permissions]: { allow: [], deny: [] } satisfies IPermissionsValue,
}),
},
};
}
private readonly _log = (msg: string) => this._logService.warn(`[AgentHostStateManager] ${msg}`);

Expand Down Expand Up @@ -228,7 +243,7 @@ export class AgentHostStateManager extends Disposable {
* The action is applied to state and emitted with the client's origin
* so the originating client can reconcile.
*/
dispatchClientAction(action: SessionAction | TerminalAction, origin: ActionOrigin): unknown {
dispatchClientAction(action: RootAction | SessionAction | TerminalAction, origin: ActionOrigin): unknown {
return this._applyAndEmit(action, origin);
}

Expand Down
4 changes: 2 additions & 2 deletions src/vs/platform/agentHost/node/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ServiceCollection } from '../../instantiation/common/serviceCollection.
import { ILogService } from '../../log/common/log.js';
import { AgentProvider, AgentSession, IAgent, IAgentCreateSessionConfig, IAgentMessageEvent, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSubagentStartedEvent, IAgentToolCompleteEvent, IAgentToolStartEvent, AuthenticateParams, AuthenticateResult } from '../common/agentService.js';
import { ISessionDataService } from '../common/sessionDataService.js';
import { ActionType, ActionEnvelope, INotification, SessionAction, TerminalAction, isSessionAction } from '../common/state/sessionActions.js';
import { ActionType, ActionEnvelope, INotification, RootAction, SessionAction, TerminalAction, isSessionAction } from '../common/state/sessionActions.js';
import type { CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../common/state/protocol/commands.js';
import { AhpErrorCodes, AHP_SESSION_NOT_FOUND, ContentEncoding, JSON_RPC_INTERNAL_ERROR, ProtocolError, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceWriteParams, type ResourceWriteResult, type IStateSnapshot } from '../common/state/sessionProtocol.js';
import { ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, buildSubagentSessionUri, parseSubagentSessionUri, type ResponsePart, type SessionConfigState, type ISessionFileDiff, type SessionSummary, type ToolCallCompletedState, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js';
Expand Down Expand Up @@ -415,7 +415,7 @@ export class AgentService extends Disposable implements IAgentService {
// in Phase 4 (multi-client). For now this is a no-op.
}

dispatchAction(action: SessionAction | TerminalAction, clientId: string, clientSeq: number): void {
dispatchAction(action: RootAction | SessionAction | TerminalAction, clientId: string, clientSeq: number): void {
this._logService.trace(`[AgentService] dispatchAction: type=${action.type}, clientId=${clientId}, clientSeq=${clientSeq}`, action);

const origin = { clientId, clientSeq };
Expand Down
7 changes: 5 additions & 2 deletions src/vs/platform/agentHost/node/copilot/copilotAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { IInstantiationService } from '../../../instantiation/common/instantiati
import { ILogService } from '../../../log/common/log.js';
import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js';
import { AgentSession, IAgent, IAgentAttachment, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentDeltaEvent, IAgentMessageEvent, IAgentModelInfo, IAgentProgressEvent, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSessionProjectInfo, IAgentSubagentStartedEvent, IAgentToolCompleteEvent, IAgentToolStartEvent } from '../../common/agentService.js';
import { AutoApproveLevel, IPermissionsValue, ISchemaProperty, createSchema, platformSessionSchema, schemaProperty } from '../../common/agentHostSchema.js';
import { AutoApproveLevel, ISchemaProperty, createSchema, platformSessionSchema, schemaProperty } from '../../common/agentHostSchema.js';
import { SessionConfigKey } from '../../common/sessionConfigKeys.js';
import { ISessionDataService, SESSION_DB_FILENAME } from '../../common/sessionDataService.js';
import type { ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../common/state/protocol/commands.js';
Expand Down Expand Up @@ -620,7 +620,10 @@ export class CopilotAgent extends Disposable implements IAgent {
const values = sessionSchema.validateOrDefault(params.config, {
[SessionConfigKey.Isolation]: isolationValue,
[SessionConfigKey.AutoApprove]: 'default' satisfies AutoApproveLevel,
[SessionConfigKey.Permissions]: { allow: [], deny: [] } satisfies IPermissionsValue,
// Permissions intentionally omitted — leave unset so auto-approval
// falls through to the host-level `permissions` default, and only
// materializes on the session once the user hits "Allow in this
// Session".
...(branchDefault !== undefined ? { [SessionConfigKey.Branch]: branchDefault } : {}),
});

Expand Down
Loading
Loading