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
186 changes: 186 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.activity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import {
ProviderInstanceId,
RunId,
ScheduledTaskId,
SECRET_REQUEST_UNSAVED_NOTE,
ThreadId,
TurnItemId,
} from "@t3tools/contracts";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as Metric from "effect/Metric";
import * as TestClock from "effect/testing/TestClock";
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import { expect, it } from "vite-plus/test";

Expand Down Expand Up @@ -655,3 +659,185 @@ it("readThread and sendToThread reach threads in other projects", async () => {
expect(stopped.code).toBe("parent_not_active");
}).pipe(Effect.provide(layer), Effect.runPromise);
});

it("requestSecret saves a pasted value to a file without opening a card", async () => {
const projection = {
thread: baseThread({
threadId: parentThreadId,
title: "Parent",
instanceId: parentInstanceId,
model: "gpt-5.4",
}),
runs: [{ ...makeRun({ id: activeRunId, ordinal: 1, status: "running" }), rootNodeId: taskId }],
visibleTurnItems: [],
runtimeRequests: [],
messages: [],
contextTransfers: [],
subagents: [],
updatedAt: now,
} as unknown as OrchestrationV2ThreadProjection;
const dispatched: Array<unknown> = [];
const saved: Array<{
readonly requestNamespace: string;
readonly requestKey: string;
readonly value: string;
}> = [];
const file = "/home/user/.t3/userdata/secrets/files/0123456789abcdef0123456789abcdef";

const layer = layerOrchestratorService.pipe(
Layer.provide(
Layer.mergeAll(
Layer.mock(ThreadManagementService.ThreadManagementService)({
getThreadRecords: (threadId) =>
threadId === parentThreadId
? Effect.succeed(projection)
: Effect.die(`unexpected thread ${threadId}`),
getThreadShell: (threadId) =>
Effect.succeed(
threadId === parentThreadId
? (projection.thread as unknown as OrchestrationV2ThreadShell)
: null,
),
dispatch: (command) => Effect.sync(() => void dispatched.push(command) as never),
} satisfies Partial<ThreadManagementService.ThreadManagementService["Service"]>),
ServerSettings.layerTest(),
Layer.mock(ProviderRegistry.ProviderRegistry)({
getProviders: Effect.succeed([]),
} satisfies Partial<ProviderRegistry.ProviderRegistry["Service"]>),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({
savePasted: (input) =>
Effect.sync(() => {
saved.push({
requestNamespace: input.requestNamespace,
requestKey: input.requestKey,
value: input.value,
});
return file;
}),
}),
Layer.mock(RemoteDelegation.RemoteDelegation)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({}),
NodeCrypto.layer,
),
),
);
const counted = Metric.snapshot.pipe(
Effect.map((snapshots) => {
const found = snapshots.find(
(snapshot) =>
snapshot.id === "t3_secret_requests_total" &&
snapshot.attributes?.status === "saved_pasted",
);
return found?.type === "Counter" ? Number(found.state.count) : 0;
}),
);

await Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
const before = yield* counted;
const result = yield* service.requestSecret(makeScope(), {
label: "Pixel PIN",
reason: "Unlocks the test phone.",
value: "482916",
clientRequestId: "pin-1",
});
expect(result).toEqual({ status: "saved", file });
expect(JSON.stringify(result)).not.toContain("482916");
expect(saved).toEqual([
{
requestNamespace: "provider-session-mcp-orchestrator-detail",
requestKey: "pin-1",
value: "482916",
},
]);
expect(dispatched).toEqual([]);
expect((yield* counted) - before).toBe(1);

const asRef = yield* service
.requestSecret(makeScope(), {
label: "Pixel PIN",
reason: "Unlocks the test phone.",
value: "482916",
delivery: "secretRef",
})
.pipe(Effect.flip);
expect(asRef.code).toBe("invalid_request");
expect(asRef.message).not.toContain("482916");
expect(saved).toHaveLength(1);
expect(dispatched).toEqual([]);
}).pipe(Effect.provide(layer), Effect.runPromise);
});

it("requestSecret returns the unsaved note for a declined, cancelled or timed-out card", async () => {
const outcomes = ["declined", "cancelled", "pending"] as const;
const results = await Effect.forEach(outcomes, (outcome) => {
const dispatched: Array<{ readonly type: string; readonly turnItemId?: string }> = [];
const projection = () => {
const card = dispatched.find((command) => command.type === "secret_request.record");
return {
thread: baseThread({
threadId: parentThreadId,
title: "Parent",
instanceId: parentInstanceId,
model: "gpt-5.4",
}),
runs: [
{ ...makeRun({ id: activeRunId, ordinal: 1, status: "running" }), rootNodeId: taskId },
],
visibleTurnItems: [],
turnItems:
card === undefined
? []
: [{ type: "secret_request", id: card.turnItemId, secretStatus: outcome }],
runtimeRequests: [],
messages: [],
contextTransfers: [],
subagents: [],
updatedAt: now,
} as unknown as OrchestrationV2ThreadProjection;
};
const layer = layerOrchestratorService.pipe(
Layer.provide(
Layer.mergeAll(
Layer.mock(ThreadManagementService.ThreadManagementService)({
getThreadRecords: () => Effect.sync(projection),
getThreadShell: () =>
Effect.sync(() => projection().thread as unknown as OrchestrationV2ThreadShell),
dispatch: (command) => Effect.sync(() => void dispatched.push(command) as never),
} satisfies Partial<ThreadManagementService.ThreadManagementService["Service"]>),
ServerSettings.layerTest(),
Layer.mock(ProviderRegistry.ProviderRegistry)({
getProviders: Effect.succeed([]),
} satisfies Partial<ProviderRegistry.ProviderRegistry["Service"]>),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(RemoteDelegation.RemoteDelegation)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({}),
NodeCrypto.layer,
),
),
);
return Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
const request = yield* service
.requestSecret(makeScope(), {
label: "Staging password",
reason: "Signs in the QA account.",
delivery: "file",
timeoutMs: 1_000,
})
.pipe(Effect.forkChild({ startImmediately: true }));
yield* TestClock.adjust("2 seconds");
return yield* Fiber.join(request);
}).pipe(Effect.provide(Layer.merge(layer, TestClock.layer())));
}).pipe(Effect.runPromise);

expect(results).toEqual([
{ status: "declined", note: SECRET_REQUEST_UNSAVED_NOTE.declined },
{ status: "cancelled", note: SECRET_REQUEST_UNSAVED_NOTE.cancelled },
{ status: "timed_out", note: SECRET_REQUEST_UNSAVED_NOTE.timed_out },
]);
});
23 changes: 22 additions & 1 deletion apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
ThreadId,
isOrchestrationV2WorkActive,
resolveProjectProviderInstanceEnabled,
SECRET_REQUEST_UNSAVED_NOTE,
} from "@t3tools/contracts";
import { getProviderOptionCurrentValue } from "@t3tools/shared/model";
import { runRanAfter } from "@t3tools/shared/orchestrationV2ThreadError";
Expand Down Expand Up @@ -2135,6 +2136,26 @@ const make = Effect.gen(function* () {
"Asking for a secret requires an active run owned by this MCP provider session.",
);
}
// A secret the user already pasted needs no card: it goes straight to a file.
if (input.value !== undefined) {
if (input.delivery === "secretRef") {
return yield* failure(
"invalid_request",
'A pasted `value` is saved as a file. Leave out `delivery: "secretRef"`.',
);
}
const file = yield* secretRequests
.savePasted({
threadId,
requestNamespace: scope.requestNamespace,
requestKey: yield* requestKey(input.clientRequestId),
value: input.value,
})
.pipe(Effect.mapError((error) => failure("orchestration_error", error.message)));
yield* Effect.annotateCurrentSpan({ "secret_request.status": "saved_pasted" });
yield* Metrics.increment(Metrics.secretRequestsTotal, { status: "saved_pasted" });
return { status: "saved" as const, file };
}
const runId = run.id;
const nodeId = run.rootNodeId;
const key = yield* requestKey(input.clientRequestId);
Expand Down Expand Up @@ -2235,7 +2256,7 @@ const make = Effect.gen(function* () {
);
yield* Effect.annotateCurrentSpan({ "secret_request.status": status });
yield* Metrics.increment(Metrics.secretRequestsTotal, { status });
if (status !== "saved") return { status };
if (status !== "saved") return { status, note: SECRET_REQUEST_UNSAVED_NOTE[status] };
if (input.delivery === "file") {
const file = yield* secretRequests
.saveToFile({ threadId, turnItemId })
Expand Down
8 changes: 5 additions & 3 deletions apps/server/src/mcp/toolkits/orchestrator/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const OrchestratorCapabilitiesTool = Tool.make("orchestrator_capabilities", {

const DelegateTaskTool = Tool.make("delegate_task", {
description:
"Start one child agent with only `task`. No parent history. Separate checkout or PR per child: `isolation:'worktree'`. Keep `taskId` for `task_status` and `task_cancel`.",
"Start one child agent with only `task`. No parent history. Separate checkout or PR per child: `isolation:'worktree'`. Keep `taskId` for `task_status` and `task_cancel`. No secrets in `task`.",
parameters: OrchestratorMcpDelegateTaskInput,
success: OrchestratorMcpDelegateTaskResult,
failure: OrchestratorMcpFailure,
Expand Down Expand Up @@ -197,15 +197,17 @@ const DeleteScheduledTaskTool = Tool.make("delete_scheduled_task", {

const RequestSecretTool = Tool.make("request_secret", {
description:
'Ask the user to enter a secret in a private card and wait. You never see the value. Returns a one-use `secretRef` for a tool that accepts it, or with `delivery: "file"` a private file path to read inside a shell command. Asked how to provide a secret: call this now, do not explain instead.',
'Ask for a secret in a private card and wait; open it, don\'t offer it. Returns a one-use `secretRef`, or with `delivery: "file"` a file to read only in its command, e.g. `adb pair <ip:port> "$(cat <file>)"`. Asked how to give one: call now. Pasted secret for child or script: `value`. Not one from an old thread.',
Comment thread
gabrielelpidio marked this conversation as resolved.
Resolved
parameters: OrchestratorMcpRequestSecretInput,
success: OrchestratorMcpRequestSecretResult,
failure: OrchestratorMcpFailure,
failureMode: "return",
dependencies,
})
.annotate(Tool.Title, "Request a secret from the user")
.annotate(Tool.Destructive, false);
.annotate(Tool.Destructive, false)
// Claude Code defers MCP tools behind ToolSearch; agents that never load this one ask in chat.
.annotate(Tool.Meta, { "anthropic/alwaysLoad": true });

const CreateThreadsTool = Tool.make("create_threads", {
description:
Expand Down
25 changes: 17 additions & 8 deletions apps/server/src/provider/AgentContextBudget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ const codexAdditionalContext = () =>
.map((entry) => entry.value)
.join("");

const layerAdvertisedTools = McpHttpServer.layerTools.pipe(
Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
Layer.provide(toolHandlerServices),
);

// Claude Code loads these tools up front instead of behind ToolSearch, outside this budget.
it.effect("only request_secret asks Claude Code to load it up front", () =>
Effect.gen(function* () {
const mcp = yield* McpServer.McpServer;
const alwaysLoad = mcp.tools
.filter(({ tool }) => tool._meta?.["anthropic/alwaysLoad"] !== undefined)
.map(({ tool }) => [tool.name, tool._meta]);
expect(alwaysLoad).toEqual([["request_secret", { "anthropic/alwaysLoad": true }]]);
}).pipe(Effect.provide(layerAdvertisedTools)),
);

// CI rejects a PR that raises any value above main's agentContextBudget.json, so growth
// needs the agent-context-increase label. Lower the values when the text shrinks.
it.effect("agent context matches the committed budget", () =>
Expand All @@ -130,12 +146,5 @@ it.effect("agent context matches the committed budget", () =>
measured,
"Set agentContextBudget.json to the measured values. Before you cut text to fit, run the evals that cover it (evals/README.md).",
).toEqual(budget);
}).pipe(
Effect.provide(
McpHttpServer.layerTools.pipe(
Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
Layer.provide(toolHandlerServices),
),
),
),
}).pipe(Effect.provide(layerAdvertisedTools)),
);
6 changes: 3 additions & 3 deletions apps/server/src/provider/agentContextBudget.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"claudeSystemAppendBytes": 5153,
"codexAdditionalContextBytes": 6883,
"mcpToolListJsonBytes": 47538
"claudeSystemAppendBytes": 5133,
"codexAdditionalContextBytes": 6863,
"mcpToolListJsonBytes": 47498
}
Loading
Loading