fix(plugins): forward plugin subagent overrides (#48277)

Merged via squash.

Prepared head SHA: ffa45893e0
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
Josh Lehman
2026-03-17 07:20:27 -07:00
committed by GitHub
parent 1561c6a71c
commit 1399ca5fcb
32 changed files with 1203 additions and 65 deletions

View File

@@ -303,6 +303,107 @@ describe("gateway agent handler", () => {
expect(capturedEntry?.acp).toEqual(existingAcpMeta);
});
it("forwards provider and model overrides for admin-scoped callers", async () => {
primeMainAgentRun();
await invokeAgent(
{
message: "test override",
agentId: "main",
sessionKey: "agent:main:main",
provider: "anthropic",
model: "claude-haiku-4-5",
idempotencyKey: "test-idem-model-override",
},
{
reqId: "test-idem-model-override",
client: {
connect: {
scopes: ["operator.admin"],
},
} as AgentHandlerArgs["client"],
},
);
const lastCall = mocks.agentCommand.mock.calls.at(-1);
expect(lastCall?.[0]).toEqual(
expect.objectContaining({
provider: "anthropic",
model: "claude-haiku-4-5",
}),
);
});
it("rejects provider and model overrides for write-scoped callers", async () => {
primeMainAgentRun();
mocks.agentCommand.mockClear();
const respond = vi.fn();
await invokeAgent(
{
message: "test override",
agentId: "main",
sessionKey: "agent:main:main",
provider: "anthropic",
model: "claude-haiku-4-5",
idempotencyKey: "test-idem-model-override-write",
},
{
reqId: "test-idem-model-override-write",
client: {
connect: {
scopes: ["operator.write"],
},
} as AgentHandlerArgs["client"],
respond,
},
);
expect(mocks.agentCommand).not.toHaveBeenCalled();
expect(respond).toHaveBeenCalledWith(
false,
undefined,
expect.objectContaining({
message: "provider/model overrides are not authorized for this caller.",
}),
);
});
it("forwards provider and model overrides when internal override authorization is set", async () => {
primeMainAgentRun();
await invokeAgent(
{
message: "test override",
agentId: "main",
sessionKey: "agent:main:main",
provider: "anthropic",
model: "claude-haiku-4-5",
idempotencyKey: "test-idem-model-override-internal",
},
{
reqId: "test-idem-model-override-internal",
client: {
connect: {
scopes: ["operator.write"],
},
internal: {
allowModelOverride: true,
},
} as AgentHandlerArgs["client"],
},
);
const lastCall = mocks.agentCommand.mock.calls.at(-1);
expect(lastCall?.[0]).toEqual(
expect.objectContaining({
provider: "anthropic",
model: "claude-haiku-4-5",
senderIsOwner: false,
}),
);
});
it("preserves cliSessionIds from existing session entry", async () => {
const existingCliSessionIds = { "claude-cli": "abc-123-def" };
const existingClaudeCliSessionId = "abc-123-def";

View File

@@ -71,6 +71,12 @@ function resolveSenderIsOwnerFromClient(client: GatewayRequestHandlerOptions["cl
return scopes.includes(ADMIN_SCOPE);
}
function resolveAllowModelOverrideFromClient(
client: GatewayRequestHandlerOptions["client"],
): boolean {
return resolveSenderIsOwnerFromClient(client) || client?.internal?.allowModelOverride === true;
}
async function runSessionResetFromAgent(params: {
key: string;
reason: "new" | "reset";
@@ -162,6 +168,8 @@ export const agentHandlers: GatewayRequestHandlers = {
const request = p as {
message: string;
agentId?: string;
provider?: string;
model?: string;
to?: string;
replyTo?: string;
sessionId?: string;
@@ -192,6 +200,21 @@ export const agentHandlers: GatewayRequestHandlers = {
inputProvenance?: InputProvenance;
};
const senderIsOwner = resolveSenderIsOwnerFromClient(client);
const allowModelOverride = resolveAllowModelOverrideFromClient(client);
const requestedModelOverride = Boolean(request.provider || request.model);
if (requestedModelOverride && !allowModelOverride) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
"provider/model overrides are not authorized for this caller.",
),
);
return;
}
const providerOverride = allowModelOverride ? request.provider : undefined;
const modelOverride = allowModelOverride ? request.model : undefined;
const cfg = loadConfig();
const idem = request.idempotencyKey;
const normalizedSpawned = normalizeSpawnedRunMetadata({
@@ -584,6 +607,8 @@ export const agentHandlers: GatewayRequestHandlers = {
ingressOpts: {
message,
images,
provider: providerOverride,
model: modelOverride,
to: resolvedTo,
sessionId: resolvedSessionId,
sessionKey: resolvedSessionKey,
@@ -619,6 +644,7 @@ export const agentHandlers: GatewayRequestHandlers = {
workspaceDir: sessionEntry?.spawnedWorkspaceDir,
}),
senderIsOwner,
allowModelOverride,
},
runId,
idempotencyKey: idem,

View File

@@ -21,6 +21,10 @@ export type GatewayClient = {
canvasHostUrl?: string;
canvasCapability?: string;
canvasCapabilityExpiresAtMs?: number;
/** Internal-only auth context that cannot be supplied through gateway RPC payloads. */
internal?: {
allowModelOverride?: boolean;
};
};
export type RespondFn = (