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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/src/mcp/McpInvocationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const ALL_MCP_CAPABILITIES = [
export type McpCapability = (typeof ALL_MCP_CAPABILITIES)[number];

export interface McpThreadCaller {
readonly boundedExecution?: boolean;
readonly threadId: ThreadId;
readonly providerSessionId: string;
readonly providerInstanceId: ProviderInstanceId;
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/mcp/McpSessionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as McpInvocationContext from "./McpInvocationContext.ts";
import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession";

export interface McpCredentialRequest {
readonly boundedExecution?: boolean;
readonly threadId: ThreadId;
readonly providerInstanceId: ProviderInstanceId;
/** False omits the preview capability wherever the token is honored. Defaults to full access. */
Expand Down Expand Up @@ -107,6 +108,7 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (
providerSessionId,
providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),
subagent: request.subagent === true,
boundedExecution: request.boundedExecution === true,
},
client: undefined,
capabilities: new Set<McpInvocationContext.McpCapability>([
Expand Down
58 changes: 58 additions & 0 deletions apps/server/src/mcp/McpToolAccess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const probe = {
],
};
const ProbeToolkit = Toolkit.make(
Tool.make("delegate_task", probe),
Tool.make("reads", probe),
Tool.make("reads_as_caller", probe),
Tool.make("acts_as_caller", probe),
Expand All @@ -132,6 +133,7 @@ const ProbeToolkit = Toolkit.make(
);
const ran = Effect.succeed({ ran: "ran" });
const probeHandlers: McpToolAccess.Handlers<typeof ProbeToolkit.tools> = {
delegate_task: McpToolAccess.actsAsCaller(() => ran),
reads: McpToolAccess.reads(() => ran),
reads_as_caller: McpToolAccess.readsAsCaller(() => ran),
acts_as_caller: McpToolAccess.actsAsCaller(() => ran),
Expand Down Expand Up @@ -178,6 +180,7 @@ export const refusedAtCompileTime = () => {
ProbeToolkit,
// @ts-expect-error a handlers layer that skipped `McpToolAccess.toLayer`
ProbeToolkit.toLayer({
delegate_task: unchecked,
reads: unchecked,
reads_as_caller: unchecked,
acts_as_caller: unchecked,
Expand Down Expand Up @@ -466,3 +469,58 @@ it.effect("refuses a linked environment to a tool that acts as its calling threa
expect(Exit.isFailure(built) && Cause.hasDies(built.cause)).toBe(true);
}),
);

it.effect("bounded credentials cannot escape through other T3 tools", () =>
Effect.gen(function* () {
const scope: McpInvocationContext.McpInvocationScope = {
environmentId: EnvironmentId.make("bounded-test"),
requestNamespace: "bounded",
issuedAt: 0,
client: undefined,
capabilities: new Set(["orchestration"]),
thread: {
threadId: ThreadId.make("bounded"),
providerInstanceId: ProviderInstanceId.make("codex"),
providerSessionId: "bounded",
boundedExecution: true,
},
};
for (const tool of ["delegate_task", "task_status", "task_cancel", "orchestrator_capabilities"])
yield* McpToolAccess.assertBoundedToolAccess(scope, tool);
for (const tool of [
"t3_thread_launch",
"create_threads",
"t3_thread_send",
"schedule_task",
"t3_terminal_send",
"request_secret",
]) {
const error = yield* McpToolAccess.assertBoundedToolAccess(scope, tool).pipe(Effect.flip);
expect(error.code).toBe("capability_denied");
}
}),
);

it.effect("enforces bounded access through the registered routed toolkit", () =>
Effect.gen(function* () {
const scope: McpInvocationContext.McpInvocationScope = {
...fullAccess,
thread: {
threadId: fullAccessThreadId,
providerInstanceId: ProviderInstanceId.make("codex"),
providerSessionId: "bounded",
boundedExecution: true,
},
};
expect(yield* call("reads", scope)).toBe("capability_denied");
expect(yield* call("delegate_task", scope)).toBe("ran");
}).pipe(
Effect.provide(
probeServer(
Layer.mock(ThreadManagement.ThreadManagementService)({
getThreadShell: (threadId) => Effect.succeed(shells.get(threadId) ?? null),
}),
),
),
),
);
31 changes: 31 additions & 0 deletions apps/server/src/mcp/McpToolAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,17 @@ const routed = Effect.fn("McpToolAccess.routed")(function* <Tools extends Record
return yield* forwarding.value.call(scope, tool, target, { ...params, environmentId });
});
}
for (const name of Object.keys(handlers)) {
const handle = handlers[name] as (
params: unknown,
) => Effect.Effect<unknown, Tool.Failure<Tools[keyof Tools]>>;
handlers[name] = (params: unknown) =>
Effect.gen(function* () {
const scope = yield* McpInvocationContext.McpInvocationContext;
yield* assertBoundedToolAccess(scope, name);
return yield* handle(params);
});
}
return handlers as Toolkit.HandlersFrom<Tools>;
});

Expand All @@ -421,3 +432,23 @@ export const toLayer = <Tools extends Record<string, Tool.Any>, EX = never, RX =
),
);
};

const boundedTools = new Set([
"orchestrator_capabilities",
"delegate_task",
"task_status",
"task_cancel",
]);

export const assertBoundedToolAccess = (
scope: McpInvocationContext.McpInvocationScope,
tool: string,
) =>
scope.thread?.boundedExecution === true && !boundedTools.has(tool)
? Effect.fail(
new OrchestratorMcpFailure({
code: "capability_denied",
message: `Bounded execution cannot call ${tool}. Use tools inside the inherited provider sandbox, or delegate_task with inherited limits.`,
}),
)
: Effect.void;
165 changes: 165 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,171 @@ describe("OrchestratorMcpService provider resolution", () => {
);
});

it.effect.each(["explicit", "inherited"] as const)(
"dispatches bounded %s approval policy to the caller's explicit local environment",
(policySource) =>
Effect.gen(function* () {
const dispatched = yield* Ref.make<ReadonlyArray<OrchestrationV2ServerCommand>>([]);
const task = {
id: taskId,
threadId: parentThreadId,
runId: parentRunId,
parentNodeId,
origin: "app_owned",
createdBy: "agent",
driver: "codex",
providerInstanceId: codexInstanceId,
providerThreadId: null,
childThreadId,
nativeTaskRef: null,
prompt: "Bounded local test",
title: null,
model: "gpt-5.4",
status: "running",
result: null,
startedAt: null,
completedAt: null,
};
const original = parentProjection([task]);
const parent = {
...original,
thread: {
...original.thread,
providerInstanceId: codexInstanceId,
runtimeMode: "auto" as const,
...(policySource === "inherited"
? {
delegatedExecution: {
providerInstanceId: codexInstanceId,
cwd: "/workspace",
sandboxPolicy: { type: "readOnly" as const, networkAccess: false },
},
}
: {}),
},
};
const dependencies = Layer.mergeAll(
NodeServices.layer,
Layer.mock(ThreadManagementService.ThreadManagementService)({
hasReceipt: () => Effect.succeed(false),
getThreadRecords: (threadId) =>
Effect.succeed(threadId === parentThreadId ? parent : childProjection),
dispatch: (command) =>
Ref.update(dispatched, (commands) => [...commands, command]).pipe(
Effect.as({
sequence: 1,
storedEvents: [
{
sequence: 1,
commandId: null,
event: { type: "subagent.updated", payload: task },
},
],
} as never),
),
}),
providerRegistryLayer([
providerSnapshot({
instanceId: codexInstanceId,
driver: ProviderDriverKind.make("codex"),
model: "gpt-5.4",
}),
]),
adapterRegistryLayer([codexInstanceId]),
ServerSettings.layerTest(),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(RemoteDelegation.RemoteDelegation)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
);
yield* Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
const result = yield* service.delegateTask(scope, {
task: "Bounded local test",
target: { environmentId: scope.environmentId },
mode: "async",
...(policySource === "explicit" ? { approvalPolicy: "never" as const } : {}),
});
assert.equal(result.status, "running");
const commands = yield* Ref.get(dispatched);
assert.lengthOf(commands, 1);
const command = commands[0];
assert.equal(command?.type, "delegated_task.request");
if (command?.type === "delegated_task.request")
assert.equal(command.approvalPolicy, "never");
}).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(dependencies))));
}),
);

it.effect.each(["worktree", "provider", "remote", "inherited-worktree"] as const)(
"rejects unsupported bounded %s before dispatch",
(kind) =>
Effect.gen(function* () {
const grokId = ProviderInstanceId.make("grok");
const original = parentProjection([]);
const parent = {
...original,
thread: {
...original.thread,
...(kind === "inherited-worktree"
? {
delegatedExecution: {
providerInstanceId: codexInstanceId,
cwd: "/workspace",
sandboxPolicy: { type: "readOnly" as const, networkAccess: false },
},
}
: {}),
},
};
const dependencies = Layer.mergeAll(
NodeServices.layer,
Layer.mock(ThreadManagementService.ThreadManagementService)({
hasReceipt: () => Effect.succeed(false),
getThreadRecords: () => Effect.succeed(parent),
dispatch: () => Effect.die("Unsupported bounded request reached dispatch"),
}),
ServerSettings.layerTest(),
providerRegistryLayer([
providerSnapshot({
instanceId: codexInstanceId,
driver: ProviderDriverKind.make("codex"),
model: "gpt-5.4",
}),
providerSnapshot({
instanceId: grokId,
driver: ProviderDriverKind.make("grok"),
model: "grok-test",
}),
]),
adapterRegistryLayer([codexInstanceId, grokId]),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(RemoteDelegation.RemoteDelegation)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
);
yield* Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
const error = yield* service
.delegateTask(scope, {
task: "Bounded test",
...(kind === "inherited-worktree" ? {} : { approvalPolicy: "never" as const }),
...(kind === "worktree" || kind === "inherited-worktree"
? { isolation: "worktree" as const }
: {}),
...(kind === "provider"
? { target: { providerInstanceId: grokId, model: "grok-test" } }
: {}),
...(kind === "remote"
? { target: { environmentId: EnvironmentId.make("remote") } }
: {}),
})
.pipe(Effect.flip);
assert.equal(error.code, "invalid_request");
}).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(dependencies))));
}),
);

it.effect("rejects delegation to a provider without a registered adapter", () =>
Effect.gen(function* () {
const forkOnlyInstanceId = ProviderInstanceId.make("forkOnly");
Expand Down
32 changes: 32 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.ts
Comment thread
jlixfeld marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2284,6 +2284,10 @@ const make = Effect.gen(function* () {
provider.supportedRuntimeModes.length === 0
? {}
: { runtimeModes: [...provider.supportedRuntimeModes] }),
delegatedApprovalPolicies:
provider.driver === "codex"
? (["inherit", "never"] as const)
: (["inherit"] as const),
canRunChildTask: constraints.length === 0,
canRunCrossProviderChildTask: constraints.length === 0,
constraints: [...constraints],
Expand All @@ -2302,10 +2306,24 @@ const make = Effect.gen(function* () {
};
}),
delegateTask: Effect.fn("OrchestratorMcpService.delegateTask")(function* (callerScope, input) {
yield* requireCapability(callerScope);
const parentCeiling =
callerScope.thread === undefined
? undefined
: (yield* threadManagement
.getThreadRecords(callerScope.thread.threadId, [])
.pipe(Effect.mapError(threadManagementFailure))).thread.delegatedExecution;
const bounded = input.approvalPolicy === "never" || parentCeiling !== undefined;
const environmentId =
input.target === undefined
? undefined
: PeerForwarding.remoteTarget(callerScope, input.target.environmentId);
if (bounded && environmentId !== undefined) {
return yield* failure(
"invalid_request",
"Bounded delegation does not support remote environments.",
);
}
if (input.target !== undefined && environmentId !== undefined) {
yield* requireCapability(callerScope);
const scope = yield* requireThreadScope(callerScope, "delegate_task");
Expand Down Expand Up @@ -2364,6 +2382,19 @@ const make = Effect.gen(function* () {
parent.thread.interactionMode,
input.interactionMode,
);
if (
bounded &&
(input.isolation === "worktree" ||
target.modelSelection.instanceId !== parent.thread.providerInstanceId ||
providers.find((provider) => provider.instanceId === target.modelSelection.instanceId)
?.driver !== "codex" ||
runtimeMode !== parent.thread.runtimeMode)
) {
return yield* failure(
"invalid_request",
"Bounded delegation requires Codex, the same provider instance, runtime mode and shared directory. No child was dispatched.",
);
}
let workspaceStrategy: OrchestrationV2ThreadLaunchWorkspaceStrategy | undefined;
if (input.isolation === "worktree" && !isRetryOfAcceptedDelegation) {
const project = yield* requireProject(parent.thread.projectId);
Expand Down Expand Up @@ -2400,6 +2431,7 @@ const make = Effect.gen(function* () {
const result = yield* threadManagement
.dispatch({
type: "delegated_task.request",
...(bounded ? { approvalPolicy: "never" as const } : {}),
createdBy: "agent",
creationSource: "mcp",
commandId,
Expand Down
Loading
Loading