Skip to content
Closed
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
8 changes: 8 additions & 0 deletions packages/provider-acp-registry/src/server/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
makeCopilotSubagentRouting,
} from "./copilotAcp.ts";
import { defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts";
import { readCustomModelEntries } from "@t3tools/shared/model";
import { AcpRegistrySettings } from "../settings.ts";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime";
Expand Down Expand Up @@ -168,6 +169,13 @@ function makeAcpRegistryRuntime(
env: { ...resolved.spawn.env, ...processEnvironment },
},
...(options.settings.authMethodId ? { authMethodId: options.settings.authMethodId } : {}),
// Custom model ids never appear in the agent-advertised choices the
// session runtime allowlists against. Let them through so the agent
// validates them (e.g. Cline accepts `cline-free/*`, which its
// discovery never advertises).
passthroughConfigValues: new Set(
readCustomModelEntries(options.settings.customModels).map((entry) => entry.slug),
),
}),
);
return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
Expand Down
12 changes: 11 additions & 1 deletion packages/provider-acp/src/server/AcpSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
decideToolCallUpdateEmission,
extractModelConfigId,
findSessionConfigOption,
isAllowedSessionConfigOptionValue,
mergeToolCallState,
toolCallProgressLength,
parseSessionModeState,
Expand Down Expand Up @@ -155,6 +156,13 @@ export interface AcpSessionRuntimeOptions {
readonly onTermination?: (error: EffectAcpErrors.AcpError) => Effect.Effect<void>;
readonly onOutgoingResponseFailure?: EffectAcpClient.AcpClientOptions["onOutgoingResponseFailure"];
readonly onOutgoingResponse?: EffectAcpClient.AcpClientOptions["onOutgoingResponse"];
/**
* Config values the provider instance explicitly configured (custom model
* ids). They bypass the advertised-choices allowlist in
* `validateConfigOptionValue` and are sent to the agent, which validates
* them. Keeps the guard for everything else.
*/
readonly passthroughConfigValues?: ReadonlySet<string>;
}

export interface AcpSessionRequestLogEvent {
Expand Down Expand Up @@ -1967,7 +1975,9 @@ export const make = (
});
}
const allowedValues = collectSessionConfigOptionValues(configOption);
if (allowedValues.includes(value)) {
if (
isAllowedSessionConfigOptionValue(configOption, value, options.passthroughConfigValues)
) {
return;
}
return yield* new EffectAcpErrors.AcpRequestError({
Expand Down
40 changes: 40 additions & 0 deletions packages/provider-acp/src/server/runtimeModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
embeddedTerminalIdsFromSessionUpdate,
extractMcpToolCallIdentity,
extractModelConfigId,
isAllowedSessionConfigOptionValue,
mergeToolCallState,
parsePermissionRequest,
parseSessionModeState,
Expand Down Expand Up @@ -1722,3 +1723,42 @@ describe("embeddedTerminalIdsFromSessionUpdate", () => {
).toBeUndefined();
});
});

describe("isAllowedSessionConfigOptionValue", () => {
const modelOption = {
id: "model",
name: "Model",
category: "model",
type: "select",
currentValue: "paid/model-a",
options: [
{ value: "paid/model-a", name: "Model A" },
{ value: "paid/model-b", name: "Model B" },
],
} as const;

it("allows advertised values without a passthrough set", () => {
expect(isAllowedSessionConfigOptionValue(modelOption, "paid/model-a", undefined)).toBe(true);
expect(isAllowedSessionConfigOptionValue(modelOption, "paid/model-a", new Set())).toBe(true);
});

it("rejects unadvertised values without a passthrough set", () => {
expect(isAllowedSessionConfigOptionValue(modelOption, "free/custom-model", undefined)).toBe(
false,
);
expect(isAllowedSessionConfigOptionValue(modelOption, "free/custom-model", new Set())).toBe(
false,
);
});

it("lets configured custom ids through even when the agent never advertises them", () => {
const passthrough = new Set(["free/custom-model"]);
expect(isAllowedSessionConfigOptionValue(modelOption, "free/custom-model", passthrough)).toBe(
true,
);
expect(isAllowedSessionConfigOptionValue(modelOption, "paid/model-a", passthrough)).toBe(true);
expect(isAllowedSessionConfigOptionValue(modelOption, "nope/elsewhere", passthrough)).toBe(
false,
);
});
});
18 changes: 18 additions & 0 deletions packages/provider-acp/src/server/runtimeModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,24 @@ export function collectSessionConfigOptionValues(
);
}

/**
* Whether a select config value may be sent to the agent. Values the provider
* instance explicitly configured (custom models) bypass the advertised-choices
* allowlist: they are provider-owned, so the agent validates them. This keeps
* the guard for everything else while allowing ids the discovery never
* advertises but the agent accepts (e.g. Cline's `cline-free/*` free tier).
*/
export function isAllowedSessionConfigOptionValue(
configOption: EffectAcpSchema.SessionConfigOption,
value: string,
passthroughValues: ReadonlySet<string> | undefined,
): boolean {
if (passthroughValues?.has(value)) {
return true;
}
return collectSessionConfigOptionValues(configOption).includes(value);
}

export function parseSessionModeState(
sessionResponse: AcpSessionSetupResponse,
): AcpSessionModeState | undefined {
Expand Down
Loading