Skip to content
52 changes: 51 additions & 1 deletion apps/server/src/mcp/OrchestratorMcpService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import * as RemoteDelegation from "../peer/RemoteDelegation.ts";
import type { McpInvocationScope } from "./McpInvocationContext.ts";
import { idleThreadProjection, liveThreadShell } from "./McpToolAccess.testkit.ts";
import * as OrchestratorMcpService from "./OrchestratorMcpService.ts";
import { resolveRuntimeMode } from "./delegatedTaskModes.ts";
import { resolveCyberAccessProgram, resolveRuntimeMode } from "./delegatedTaskModes.ts";
import type { ProjectionRecordFilter } from "../orchestration-v2/ProjectionStore.ts";
import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";

Expand Down Expand Up @@ -3253,6 +3253,56 @@ describe("OrchestratorMcpService provider resolution", () => {
}),
);
});

describe("resolveCyberAccessProgram", () => {
const codex = (options?: ReadonlyArray<{ id: string; value: string }>) => ({
instanceId: codexInstanceId,
model: "gpt-6-sol",
...(options === undefined ? {} : { options }),
});
const requested = (value: string) => [{ id: "cyberAccessProgram", value }];

it.effect("refuses a cyber access program the parent thread did not choose", () =>
Effect.gen(function* () {
const error = yield* resolveCyberAccessProgram(
codex(),
codex(requested("daybreakRed")),
).pipe(Effect.flip);
assert.equal(error.code, "invalid_request");
const switched = yield* resolveCyberAccessProgram(
codex(requested("daybreakBlue")),
codex(requested("daybreakRed")),
).pipe(Effect.flip);
assert.equal(switched.code, "invalid_request");
const duplicated = yield* resolveCyberAccessProgram(
codex(),
codex([...requested("default"), ...requested("daybreakBlue")]),
).pipe(Effect.flip);
assert.equal(duplicated.code, "invalid_request");
// The model may not offer the parent's program, so it does not carry over.
const otherModel = yield* resolveCyberAccessProgram(codex(requested("daybreakBlue")), {
...codex(requested("daybreakBlue")),
model: "gpt-6.1-sol",
}).pipe(Effect.flip);
assert.equal(otherModel.code, "invalid_request");
}),
);

it.effect("allows the parent's program, the default, or no request", () =>
Effect.gen(function* () {
const parent = codex(requested("daybreakBlue"));
yield* resolveCyberAccessProgram(parent, codex(requested("daybreakBlue")));
yield* resolveCyberAccessProgram(parent, {
...codex(requested("default")),
model: "gpt-6.1-sol",
});
yield* resolveCyberAccessProgram(
codex(),
codex([{ id: "reasoningEffort", value: "high" }]),
);
}),
);
});
});

describe("OrchestratorMcpService thread lookup", () => {
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import {
import * as Metrics from "../observability/Metrics.ts";
import {
interactionModeRank,
resolveCyberAccessProgram,
resolveInteractionMode,
resolveRuntimeMode,
runtimeModeRank,
Expand Down Expand Up @@ -1443,6 +1444,11 @@ const make = Effect.gen(function* () {
`Model ${model} on provider ${instanceId} rejected options: ${invalid.join(" ")}`,
);
}
yield* resolveCyberAccessProgram(inheritedSelection, {
instanceId,
model,
options: requestedOptions,
});
}

const inheritsOptions =
Expand Down
37 changes: 37 additions & 0 deletions apps/server/src/mcp/delegatedTaskModes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
type OrchestratorMcpInteractionMode,
type OrchestratorMcpRuntimeMode,
type ProviderInteractionMode,
type ProviderOptionSelection,
type RuntimeMode,
} from "@t3tools/contracts";
import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
import * as Effect from "effect/Effect";

/** How much a runtime mode allows, narrowest first. */
Expand Down Expand Up @@ -87,6 +89,41 @@ export function resolveInteractionMode(
: Effect.succeed(resolved);
}

/**
* A child may keep its parent's cyber access program on the same instance and model, but not
* choose another; only the user can. Codex grants programs per account and model.
*/
export function resolveCyberAccessProgram(
parentSelection: ModelSelection | undefined,
requested: Pick<ModelSelection, "instanceId" | "model"> & {
readonly options?: ReadonlyArray<ProviderOptionSelection> | undefined;
},
): Effect.Effect<void, OrchestratorMcpFailure> {
const sameModel =
parentSelection?.instanceId === requested.instanceId &&
parentSelection.model === requested.model;
const parentValue =
(sameModel
? getModelSelectionStringOptionValue(parentSelection, "cyberAccessProgram")
: undefined) ?? "default";
// Check every entry: a later duplicate would win wherever options are merged.
const chosen = (requested.options ?? []).find(
(selection) =>
selection.id === "cyberAccessProgram" &&
selection.value !== "default" &&
selection.value !== parentValue,
);
return chosen === undefined
? Effect.void
: Effect.fail(
new OrchestratorMcpFailure({
code: "invalid_request",
message:
"The requested cyber access program is not valid for this thread. Omit it; the user chooses one in the composer's model options.",
}),
);
}

/** The task as its child receives it, with the role the caller asked for. */
export function taskPrompt(input: OrchestratorMcpDelegateTaskInput): string {
return input.role === undefined || input.role === "general"
Expand Down
25 changes: 25 additions & 0 deletions apps/server/src/mcp/toolkits/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,25 @@ it.effect("a client caller targets any thread within its ceiling and cannot act
code: "runtime_mode_escalation_denied",
});

// The thread runs Daybreak Blue on gpt-6-sol: configure may keep it, not move it.
const configureCyber = (model: string) =>
call(
"t3_thread_configure",
{
threadId: "other-project-thread",
modelSelection: {
instanceId: "codex",
model,
options: [{ id: "cyberAccessProgram", value: "daybreakBlue" }],
},
},
clientScope("auto"),
);
expect((yield* configureCyber("gpt-6-sol")).isError).toBe(false);
expect(declaredFailure(yield* configureCyber("gpt-6.1-sol"))).toMatchObject({
code: "invalid_request",
});

const forked = yield* call(
"t3_thread_transfer",
{ action: "fork", sourcePoint: { type: "latest_stable" } },
Expand All @@ -545,6 +564,12 @@ it.effect("a client caller targets any thread within its ceiling and cannot act
thread: {
id: ThreadId.make("other-project-thread"),
projectId: "other-project",
providerInstanceId: "codex",
modelSelection: {
instanceId: "codex",
model: "gpt-6-sol",
options: [{ id: "cyberAccessProgram", value: "daybreakBlue" }],
},
runtimeMode: "auto",
interactionMode: "default",
deletedAt: null,
Expand Down
54 changes: 54 additions & 0 deletions apps/server/src/mcp/toolkits/project/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ it.effect(
readonly targeted?: false;
readonly overrideModel?: ModelSelection;
readonly hostedAppUrl?: string;
readonly modelSelection?: ModelSelection;
readonly refusedByCeiling?: true;
},
) {
Expand Down Expand Up @@ -248,6 +249,17 @@ it.effect(
expect(escalated.result).toMatchObject({ code: "runtime_mode_escalation_denied" });
expect(escalated.launched).toHaveLength(0);

const cyber = yield* launchWith("full-access", "full-access", {
title: "Fix",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-6-sol",
options: [{ id: "cyberAccessProgram", value: "daybreakBlue" }],
},
});
expect(cyber.result).toMatchObject({ code: "invalid_request" });
expect(cyber.launched).toHaveLength(0);

const untargeted = yield* launchWith("full-access", "full-access", {
title: "Fix",
targeted: false,
Expand Down Expand Up @@ -276,6 +288,31 @@ it.effect(
}),
);

it.effect("a client cannot make a cyber access program a project's default", () =>
Effect.gen(function* () {
const { projectId, dependencies } = clientLaunchHarness({
runtimeModeCeiling: "full-access",
launched: [],
});
const toolkit = yield* ProjectToolkit.pipe(
Effect.provide(
McpToolAccess.HandlersLayer.layer(ProjectHandlers.layer).pipe(Layer.provide(dependencies)),
),
);
const result = yield* toolkit
.handle("t3_project_update", {
projectId,
defaultModelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-6-sol",
options: [{ id: "cyberAccessProgram", value: "daybreakBlue" }],
},
})
.pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies));
expect(result.at(-1)?.result).toMatchObject({ code: "invalid_request" });
}),
);

it.effect("a launch steps an inherited mode down to the provider's, and refuses one it lacks", () =>
Effect.gen(function* () {
// Pi offers no Auto; a mode a provider lacks would run as Supervised.
Expand Down Expand Up @@ -367,6 +404,23 @@ it.effect("a launch retried with the same clientRequestId replays the first one"
expect(ids[2]).not.toEqual(ids[0]);
// Without a key every launch is new.
expect(ids[3]).not.toEqual(ids[4]);

// A retry still cannot choose a cyber access program.
const retried = yield* toolkit
.handle("t3_thread_launch", {
title: "Fix",
projectId,
message: "Fix the bug",
clientRequestId: "fix-1",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-6-sol",
options: [{ id: "cyberAccessProgram", value: "daybreakBlue" }],
},
})
.pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies));
expect(retried.at(-1)?.result).toMatchObject({ code: "invalid_request" });
expect(launched).toHaveLength(5);
}),
);

Expand Down
21 changes: 17 additions & 4 deletions apps/server/src/mcp/toolkits/project/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import * as ProviderRegistry from "../../../provider/ProviderRegistry.ts";
import * as DateTime from "effect/DateTime";
import { resolveNewThreadModelSelection } from "@t3tools/shared/usageLimits";
import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
import { resolveRuntimeMode } from "../../delegatedTaskModes.ts";
import { resolveCyberAccessProgram, resolveRuntimeMode } from "../../delegatedTaskModes.ts";
import * as McpInvocationContext from "../../McpInvocationContext.ts";
import * as McpToolAccess from "../../McpToolAccess.ts";
import { newCommandId, readCaller, resolveProjectId, unavailable } from "../../threadAccess.ts";
Expand Down Expand Up @@ -131,9 +131,9 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
Effect.mapError(unavailable),
Effect.map(Option.getOrUndefined),
);
// A retry of an accepted launch replays it: it keeps the model it started
// with and skips the checks only a new launch needs, which the project
// may no longer pass, such as a base branch deleted since.
// A retry of an accepted launch replays it: without a modelSelection it keeps
// the launched model, and it skips the checks only a new launch needs, which
// the project may no longer pass, such as a base branch deleted since.
const launched =
input.clientRequestId === undefined
? null
Expand Down Expand Up @@ -208,6 +208,9 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
message:
"Pass modelSelection: the project has no default model. orchestrator_capabilities lists providers and models.",
});
// A retry's selection still reaches a new run, so replays are checked too.
if (input.modelSelection !== undefined)
yield* resolveCyberAccessProgram(caller?.modelSelection, input.modelSelection);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
let startedRuntimeMode =
projectDefaults !== undefined && input.runtimeMode === undefined
? yield* resolveRuntimeMode(runtimeMode, projectDefaults.defaultRuntimeMode).pipe(
Expand Down Expand Up @@ -345,6 +348,8 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
...(created.commitError === undefined ? {} : { commitError: created.commitError }),
};
}
if (input.defaultModelSelection)
yield* resolveCyberAccessProgram(undefined, input.defaultModelSelection);
const commandId = yield* newCommandId();
return yield* projects
.create({ ...input, workspaceRoot, commandId, projectId: ProjectId.make(commandId) })
Expand All @@ -357,6 +362,14 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
t3_project_update: McpToolAccess.writesEnvironment((input) =>
Effect.gen(function* () {
const projects = yield* Project.ProjectService;
const requested = input.defaultModelSelection;
if (requested) {
const current = yield* projects.getById(input.projectId).pipe(Effect.mapError(unavailable));
yield* resolveCyberAccessProgram(
Option.getOrUndefined(current)?.defaultModelSelection ?? undefined,
requested,
);
}
return yield* projects
.update({ ...input, commandId: yield* newCommandId() })
.pipe(Effect.mapError(projectFailure));
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/mcp/toolkits/thread/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import * as Effect from "effect/Effect";
import { modelSelectionCommandType } from "@t3tools/shared/model";

import { resolveCyberAccessProgram } from "../../delegatedTaskModes.ts";
import * as McpInvocationContext from "../../McpInvocationContext.ts";
import * as McpToolAccess from "../../McpToolAccess.ts";
import {
Expand Down Expand Up @@ -218,6 +219,7 @@ export const layer = McpToolAccess.toLayer(ThreadToolkit, {
threads,
projection: { thread },
} = yield* readThread(input.threadId);
yield* resolveCyberAccessProgram(thread.modelSelection, modelSelection);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const type = modelSelectionCommandType(thread.providerInstanceId, modelSelection);
const result = yield* threads
.dispatch({ type, threadId: thread.id, commandId: yield* newCommandId(), modelSelection })
Expand Down
25 changes: 25 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,31 @@ describe("CodexAdapterV2 runtime policy", () => {
assert.isDefined(withT3.additionalContext?.["t3_code_runtime"]);
}),
);

it.effect("requests a cyber access program only when one is selected", () =>
Effect.gen(function* () {
const build = (cyberAccessProgram: string | undefined) =>
CodexAdapterV2.buildCodexTurnStartParams({
nativeThreadId: "native-cyber-access",
codexInput: [{ type: "text", text: "test" }],
runtimePolicy: { runtimeMode: "full-access", interactionMode: "default", cwd: null },
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-6-sol",
...(cyberAccessProgram === undefined
? {}
: { options: [{ id: "cyberAccessProgram", value: cyberAccessProgram }] }),
},
});

assert.notProperty(yield* build(undefined), "cyberAccessProgram");
assert.notProperty(yield* build("default"), "cyberAccessProgram");
assert.notProperty(yield* build("standard"), "cyberAccessProgram");
assert.notProperty(yield* build("daybreakPurple"), "cyberAccessProgram");
assert.equal((yield* build("daybreakBlue")).cyberAccessProgram, "daybreakBlue");
assert.equal((yield* build("daybreakRed")).cyberAccessProgram, "daybreakRed");
}),
);
});

describe("CodexAdapterV2 native protocol logging", () => {
Expand Down
Loading
Loading