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
67 changes: 65 additions & 2 deletions src/server/handlers/request/agent-stream.handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ describe("server/handlers/request/agent-stream.handler", () => {
assertEquals(injectedToolSchema, inputSchema);
});

it("passes runtime integration tool allowlists from forwarded props into the runtime agent config", async () => {
it("does not pass undeclared forwarded remote tool allowlists into the runtime agent config", async () => {
let capturedAllowedTools: string[] | undefined;

const handler = new AgentStreamHandler({
Expand Down Expand Up @@ -590,7 +590,70 @@ describe("server/handlers/request/agent-stream.handler", () => {

assertExists(result.response);
assertEquals(result.response.status, 200);
assertEquals(capturedAllowedTools, ["gmail:list-emails", "gmail:get-email"]);
assertEquals(capturedAllowedTools, undefined);
});

it("drops undeclared Studio runtime tool allowlists for untrusted clients", async () => {
let capturedAllowedTools: string[] | undefined;

const handler = new AgentStreamHandler({
ensureProjectDiscovery: async () => {},
getAgent: (id) => id === "assistant-1" ? createAgent("assistant-1") : undefined,
getAllAgentIds: () => ["assistant-1"],
sessionManager: new AgentRunSessionManager(),
createRuntime: (agent) => {
capturedAllowedTools = (agent.config as typeof agent.config & RuntimeRemoteToolConfig)
.__vfAllowedRemoteTools;

return {
stream: async (_messages, _context, callbacks) => {
callbacks?.onFinish?.({
text: "ok",
messages: [],
toolCalls: [],
status: "completed",
usage: {
promptTokens: 1,
completionTokens: 1,
totalTokens: 2,
},
});

return new ReadableStream<Uint8Array>({
start(controller) {
controller.close();
},
});
},
};
},
});

const body = createAgentStreamRequestBody({
forwardedProps: {
clientId: "external-client",
runtimeOverrides: {
allowedTools: ["studio_todo_write"],
},
},
});
const { jws, publicKeyPem } = await createControlPlaneSignature(body, { requestId: "run_1" });

const result = await handler.handle(
new Request("https://example.com/api/control-plane/runs/run_1/stream", {
method: "POST",
headers: {
"content-type": "application/json",
"x-veryfront-control-plane-jws": jws,
},
body,
}),
createCtx(publicKeyPem),
);

assertExists(result.response);
assertEquals(result.response.status, 200);
assertEquals(capturedAllowedTools, undefined);
});

it("auto-exposes Studio MCP tools for trusted Studio project-agent requests", async () => {
Expand Down
72 changes: 71 additions & 1 deletion src/server/handlers/request/agent-stream.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
getInternalAgentStreamRequestSchema,
type InternalAgentStreamRequest,
type RuntimeAgentSourceContext,
type RuntimeRunAgentInput,
toRuntimeRunAgentInput,
} from "#veryfront/internal-agents/schema.ts";
import { BaseHandler } from "../response/base.ts";
Expand Down Expand Up @@ -187,6 +188,75 @@ function getRequestedStudioToolNames(input: {
.sort();
}

function sanitizeForwardedRuntimeAllowedTools(input: {
forwardedProps?: Record<string, unknown>;
availableToolNames: string[];
allowStudioRuntimeTools: boolean;
}): Record<string, unknown> | undefined {
const forwardedProps = input.forwardedProps;
if (!isRecord(forwardedProps)) {
return forwardedProps;
}

const runtimeOverrides = isRecord(forwardedProps.runtimeOverrides)
? forwardedProps.runtimeOverrides
: null;
if (!runtimeOverrides || !Object.hasOwn(runtimeOverrides, "allowedTools")) {
return forwardedProps;
}

const allowedTools = runtimeOverrides.allowedTools;
if (
!Array.isArray(allowedTools) || !allowedTools.every((toolName) => typeof toolName === "string")
) {
return forwardedProps;
}

const availableToolNames = new Set(input.availableToolNames);
// Platform remote tools are gated separately by the child agent config in
// withVeryfrontPlatformRemoteTools. The Studio path is the one that consumes
// forwarded allowedTools, and Studio-only runtime tools are preserved only
// for trusted Studio clients that can already attach the Studio MCP surface.
const sanitizedAllowedTools = allowedTools.filter((toolName) =>
availableToolNames.has(toolName) ||
(input.allowStudioRuntimeTools && STUDIO_RUNTIME_REMOTE_TOOL_NAMES.has(toolName))
);
if (sanitizedAllowedTools.length === allowedTools.length) {
return forwardedProps;
}

const nextRuntimeOverrides: Record<string, unknown> = {
...runtimeOverrides,
allowedTools: sanitizedAllowedTools,
};
if (sanitizedAllowedTools.length === 0) {
delete nextRuntimeOverrides.allowedTools;
}

const nextForwardedProps: Record<string, unknown> = {
...forwardedProps,
runtimeOverrides: nextRuntimeOverrides,
};
if (Object.keys(nextRuntimeOverrides).length === 0) {
delete nextForwardedProps.runtimeOverrides;
}

return Object.keys(nextForwardedProps).length > 0 ? nextForwardedProps : undefined;
}

function sanitizeRuntimeRunAgentInput(input: RuntimeRunAgentInput): RuntimeRunAgentInput {
const clientProfile = resolveRuntimeClientProfile(input.forwardedProps);

return {
...input,
forwardedProps: sanitizeForwardedRuntimeAllowedTools({
forwardedProps: input.forwardedProps,
availableToolNames: input.tools.map((tool) => tool.name),
allowStudioRuntimeTools: clientAllowsStudioMcp(clientProfile),
}),
};
}

function getVeryfrontApiMcpPolicy(agent: Agent): {
allowAll: boolean;
requestedToolNames: string[];
Expand Down Expand Up @@ -559,7 +629,7 @@ export class AgentStreamHandler extends BaseHandler {
return this.respond(builder.json({ error: "Agent not found" }, 404));
}

const runtimeInput = toRuntimeRunAgentInput(payload);
const runtimeInput = sanitizeRuntimeRunAgentInput(toRuntimeRunAgentInput(payload));
const apiAuthToken = payload.credentials?.authToken || ctx.proxyToken ||
getHostEnv("VERYFRONT_API_TOKEN") || "";
const platformRuntimeAgent = await withVeryfrontPlatformRemoteTools({
Expand Down