mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Fix forward-compatible provider handling for old app clients
AgentProviderSchema was z.enum() which caused old clients to reject session messages containing unknown providers (pi, copilot), breaking the entire session. Changed to z.string() for future clients. For currently deployed clients (<0.1.45), the daemon now filters out unknown providers based on the appVersion sent in the WebSocket hello message. Clients that don't send appVersion only see claude/codex/opencode.
This commit is contained in:
@@ -155,6 +155,7 @@ export type DaemonClientConfig = {
|
||||
url: string;
|
||||
clientId: string;
|
||||
clientType?: "mobile" | "browser" | "cli" | "mcp";
|
||||
appVersion?: string;
|
||||
runtimeGeneration?: number | null;
|
||||
authHeader?: string;
|
||||
suppressSendErrors?: boolean;
|
||||
@@ -3285,6 +3286,7 @@ export class DaemonClient {
|
||||
clientId: this.config.clientId,
|
||||
clientType: this.config.clientType ?? "cli",
|
||||
protocolVersion: 1,
|
||||
...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}),
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -176,12 +176,9 @@ export function getAgentProviderDefinition(provider: string): AgentProviderDefin
|
||||
return definition;
|
||||
}
|
||||
|
||||
export const AGENT_PROVIDER_IDS = AGENT_PROVIDER_DEFINITIONS.map((d) => d.id) as [
|
||||
string,
|
||||
...string[],
|
||||
];
|
||||
export const AGENT_PROVIDER_IDS = AGENT_PROVIDER_DEFINITIONS.map((d) => d.id);
|
||||
|
||||
export const AgentProviderSchema = z.enum(AGENT_PROVIDER_IDS);
|
||||
export const AgentProviderSchema = z.string();
|
||||
|
||||
export function isValidAgentProvider(value: string): boolean {
|
||||
return AGENT_PROVIDER_IDS.includes(value);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
|
||||
|
||||
import { AgentProviderRuntimeSettingsMapSchema } from "./agent/provider-launch-config.js";
|
||||
|
||||
const LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
||||
@@ -82,7 +82,7 @@ const FeatureVoiceModeSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
llm: z
|
||||
.object({
|
||||
provider: z.enum(AGENT_PROVIDER_IDS as [string, ...string[]]).optional(),
|
||||
provider: z.string().optional(),
|
||||
model: z.string().min(1).optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
@@ -190,6 +190,28 @@ const execFileAsync = promisify(execFile);
|
||||
const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS);
|
||||
const pendingAgentInitializations = new Map<string, Promise<ManagedAgent>>();
|
||||
const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
|
||||
|
||||
// TODO: Remove once all app store clients are on >=0.1.45 and understand arbitrary provider strings.
|
||||
// Clients before 0.1.45 validate providers with z.enum(["claude", "codex", "opencode"]) and reject
|
||||
// the entire session message if they encounter an unknown provider.
|
||||
const LEGACY_PROVIDER_IDS = new Set(["claude", "codex", "opencode"]);
|
||||
const MIN_VERSION_ALL_PROVIDERS = "0.1.45";
|
||||
|
||||
function clientSupportsAllProviders(appVersion: string | null): boolean {
|
||||
if (!appVersion) return false;
|
||||
// Strip RC/prerelease suffix: "0.1.45-rc.4" → "0.1.45"
|
||||
const base = appVersion.replace(/-.*$/, "");
|
||||
const parts = base.split(".").map(Number);
|
||||
const minParts = MIN_VERSION_ALL_PROVIDERS.split(".").map(Number);
|
||||
for (let i = 0; i < minParts.length; i++) {
|
||||
const a = parts[i] ?? 0;
|
||||
const b = minParts[i] ?? 0;
|
||||
if (a > b) return true;
|
||||
if (a < b) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
|
||||
const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__";
|
||||
const TERMINAL_STREAM_HIGH_WATER_BYTES = 256 * 1024;
|
||||
@@ -355,6 +377,7 @@ type VoiceTranscriptionResultPayload = {
|
||||
|
||||
export type SessionOptions = {
|
||||
clientId: string;
|
||||
appVersion: string | null;
|
||||
onMessage: (msg: SessionOutboundMessage) => void;
|
||||
onBinaryMessage?: (frame: Uint8Array) => void;
|
||||
getBinaryBufferedAmount?: () => number;
|
||||
@@ -506,6 +529,7 @@ function toAgentPersistenceHandle(
|
||||
*/
|
||||
export class Session {
|
||||
private readonly clientId: string;
|
||||
private readonly appVersion: string | null;
|
||||
private readonly sessionId: string;
|
||||
private readonly onMessage: (msg: SessionOutboundMessage) => void;
|
||||
private readonly onBinaryMessage: ((frame: Uint8Array) => void) | null;
|
||||
@@ -601,6 +625,7 @@ export class Session {
|
||||
constructor(options: SessionOptions) {
|
||||
const {
|
||||
clientId,
|
||||
appVersion,
|
||||
onMessage,
|
||||
onBinaryMessage,
|
||||
getBinaryBufferedAmount,
|
||||
@@ -627,6 +652,7 @@ export class Session {
|
||||
agentProviderRuntimeSettings,
|
||||
} = options;
|
||||
this.clientId = clientId;
|
||||
this.appVersion = appVersion;
|
||||
this.sessionId = uuidv4();
|
||||
this.onMessage = onMessage;
|
||||
this.onBinaryMessage = onBinaryMessage ?? null;
|
||||
@@ -1130,6 +1156,12 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove once all app store clients are on >=0.1.45.
|
||||
private isProviderVisibleToClient(provider: string): boolean {
|
||||
if (clientSupportsAllProviders(this.appVersion)) return true;
|
||||
return LEGACY_PROVIDER_IDS.has(provider);
|
||||
}
|
||||
|
||||
private matchesAgentFilter(options: {
|
||||
agent: AgentSnapshotPayload;
|
||||
project: ProjectPlacementPayload;
|
||||
@@ -1198,6 +1230,11 @@ export class Session {
|
||||
subscription: AgentUpdatesSubscriptionState,
|
||||
payload: AgentUpdatePayload,
|
||||
): void {
|
||||
// TODO: Remove once all app store clients are on >=0.1.45.
|
||||
if (payload.kind === "upsert" && !this.isProviderVisibleToClient(payload.agent.provider)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscription.isBootstrapping) {
|
||||
subscription.pendingUpdatesByAgentId.set(this.getAgentUpdateTargetId(payload), payload);
|
||||
return;
|
||||
@@ -3179,7 +3216,11 @@ export class Session {
|
||||
): Promise<void> {
|
||||
const fetchedAt = new Date().toISOString();
|
||||
try {
|
||||
const providers = await this.agentManager.listProviderAvailability();
|
||||
let providers = await this.agentManager.listProviderAvailability();
|
||||
|
||||
// TODO: Remove once all app store clients are on >=0.1.45.
|
||||
providers = providers.filter((p) => this.isProviderVisibleToClient(p.provider));
|
||||
|
||||
this.emit({
|
||||
type: "list_available_providers_response",
|
||||
payload: {
|
||||
@@ -5691,6 +5732,12 @@ export class Session {
|
||||
}
|
||||
|
||||
const payload = await this.listFetchAgentsEntries(request);
|
||||
|
||||
// TODO: Remove once all app store clients are on >=0.1.45.
|
||||
payload.entries = payload.entries.filter((entry) =>
|
||||
this.isProviderVisibleToClient(entry.agent.provider),
|
||||
);
|
||||
|
||||
const snapshotUpdatedAtByAgentId = new Map<string, number>();
|
||||
for (const entry of payload.entries) {
|
||||
const parsedUpdatedAt = Date.parse(entry.agent.updatedAt);
|
||||
|
||||
@@ -176,6 +176,7 @@ type WebSocketLike = {
|
||||
type SessionConnection = {
|
||||
session: Session;
|
||||
clientId: string;
|
||||
appVersion: string | null;
|
||||
connectionLogger: pino.Logger;
|
||||
sockets: Set<WebSocketLike>;
|
||||
externalDisconnectCleanupTimeout: ReturnType<typeof setTimeout> | null;
|
||||
@@ -595,13 +596,15 @@ export class VoiceAssistantWebSocketServer {
|
||||
private createSessionConnection(params: {
|
||||
ws: WebSocketLike;
|
||||
clientId: string;
|
||||
appVersion: string | null;
|
||||
connectionLogger: pino.Logger;
|
||||
}): SessionConnection {
|
||||
const { ws, clientId, connectionLogger } = params;
|
||||
const { ws, clientId, appVersion, connectionLogger } = params;
|
||||
let connection: SessionConnection | null = null;
|
||||
|
||||
const session = new Session({
|
||||
clientId,
|
||||
appVersion,
|
||||
onMessage: (msg) => {
|
||||
if (!connection) {
|
||||
return;
|
||||
@@ -677,6 +680,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
connection = {
|
||||
session,
|
||||
clientId,
|
||||
appVersion,
|
||||
connectionLogger,
|
||||
sockets: new Set([ws]),
|
||||
externalDisconnectCleanupTimeout: null,
|
||||
@@ -760,6 +764,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
const connection = this.createSessionConnection({
|
||||
ws,
|
||||
clientId,
|
||||
appVersion: message.appVersion ?? null,
|
||||
connectionLogger,
|
||||
});
|
||||
this.sessions.set(ws, connection);
|
||||
|
||||
@@ -2653,6 +2653,7 @@ export const WSHelloMessageSchema = z.object({
|
||||
clientId: z.string().min(1),
|
||||
clientType: z.enum(["mobile", "browser", "cli", "mcp"]),
|
||||
protocolVersion: z.number().int(),
|
||||
appVersion: z.string().optional(),
|
||||
capabilities: z
|
||||
.object({
|
||||
voice: z.boolean().optional(),
|
||||
|
||||
Reference in New Issue
Block a user