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
25 changes: 25 additions & 0 deletions apps/server/src/provider/Drivers/AntigravityDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ANTIGRAVITY_DEFAULT_MODEL,
ProviderInstanceId,
type AntigravitySettings,
ThreadId,
} from "@t3tools/contracts";
import {
HostProcessEnvironment,
Expand All @@ -24,6 +25,7 @@ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawne
import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { GitHubCliAccountEnvironment } from "../../sourceControl/GitHubCli.ts";
import {
AntigravityInstallation,
AntigravityInstallationError,
Expand Down Expand Up @@ -126,6 +128,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
forceFileStorage: string | undefined;
credentialKeys: ReadonlyArray<string>;
geminiApiKey: string | undefined;
gitHubToken: string | undefined;
tempDirectory: string | undefined;
handle: ChildProcessSpawner.ChildProcessHandle;
}> = [];
Expand Down Expand Up @@ -179,6 +182,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
blockedCredentialKeys.has(key.toUpperCase()),
),
geminiApiKey: environment.GEMINI_API_KEY,
gitHubToken: environment.GH_TOKEN,
// Only the agent gets a per-process temp directory. Other launches
// inherit the host TMPDIR.
tempDirectory:
Expand Down Expand Up @@ -277,6 +281,27 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
),
);

it.effect.skipIf(windowsHost)(
"starts session agents as the checkout's selected GitHub CLI login",
() =>
Effect.gen(function* () {
const h = yield* makeHarness({ enabled: true }).pipe(
Effect.provideService(GitHubCliAccountEnvironment, {
forCwd: () => Effect.succeed({ GH_TOKEN: "selected", GITHUB_TOKEN: "selected" }),
}),
);
const threadId = ThreadId.make("antigravity-github-login");
yield* h.instance.adapter.startSession({
threadId,
cwd: process.cwd(),
runtimeMode: "full-access",
});
yield* h.instance.adapter.stopSession(threadId);
const agentLaunches = h.launches.filter((launch) => launch.harnessPath !== undefined);
expect(agentLaunches.map((launch) => launch.gitHubToken)).toEqual(["selected"]);
}).pipe(Effect.scoped),
);

it.effect.skipIf(windowsHost)("does not launch a process for a disabled instance", () =>
Effect.gen(function* () {
const h = yield* makeHarness();
Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,10 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
installation: executable,
profile,
cwd: input.cwd,
baseEnv: withAgentDeviceEnvironment(processEnvironment, input),
baseEnv: withAgentDeviceEnvironment(
{ ...processEnvironment, ...input.gitHubEnvironment },
input,
),
auth,
runtimeTempDirectory,
}),
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/provider/Layers/AntigravityAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {
selectAntigravityPermissionOptionId,
} from "../acp/AntigravityProtocol.ts";
import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts";
import { GitHubCliAccountEnvironment } from "../../sourceControl/GitHubCli.ts";
import type { EventNdjsonLogger } from "./EventNdjsonLogger.ts";

const PROVIDER = ProviderDriverKind.make("antigravity");
Expand Down Expand Up @@ -310,6 +311,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const serverConfig = yield* ServerConfig;
const gitHubAccountEnvironment = yield* GitHubCliAccountEnvironment;
const ownerScope = yield* Effect.scope;
const makeNativeLoggers = yield* makeAcpNativeLoggerFactory();
const sessions = new Map<ThreadId, SessionContext>();
Expand Down Expand Up @@ -786,6 +788,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
stopOwned,
Effect.gen(function* () {
const mcp = McpProviderSession.readMcpProviderSession(input.threadId);
const gitHubEnvironment = yield* gitHubAccountEnvironment.forCwd(cwd);
// The attachments dir grant lets the agent read path-only uploads
// at the paths ProviderService injects into the turn text. It is
// a leaf directory holding only uploads.
Expand All @@ -796,6 +799,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
...(mcp?.agentDeviceEnvironment
? { agentDeviceEnvironment: mcp.agentDeviceEnvironment }
: {}),
gitHubEnvironment,
additionalDirectories: [serverConfig.attachmentsDir],
...(Option.isSome(cursor) ? { resumeSessionId: cursor.value.sessionId } : {}),
mcpServers: mcp
Expand Down
31 changes: 30 additions & 1 deletion apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import * as TestClock from "effect/testing/TestClock";

import { attachmentRelativePath } from "../../attachmentStore.ts";
import { ServerConfig } from "../../config.ts";
import { GitHubCliAccountEnvironment } from "../../sourceControl/GitHubCli.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import {
SYNTHETIC_CLAUDE_CAPABLE_MODEL,
Expand Down Expand Up @@ -173,6 +174,7 @@ function makeHarness(config?: {
readonly environment?: ClaudeAdapterLiveOptions["environment"];
readonly getSessionMessages?: ClaudeAdapterLiveOptions["getSessionMessages"];
readonly forkSession?: ClaudeAdapterLiveOptions["forkSession"];
readonly gitHubAccountEnvironment?: Readonly<Record<string, string>>;
}) {
const query = new FakeClaudeQuery();
const queries = [query];
Expand Down Expand Up @@ -212,7 +214,11 @@ function makeHarness(config?: {
ClaudeAdapter,
Effect.gen(function* () {
const claudeConfig = decodeClaudeSettings(config?.claudeConfig ?? {});
return yield* makeClaudeAdapter(claudeConfig, adapterOptions);
return yield* makeClaudeAdapter(claudeConfig, adapterOptions).pipe(
Effect.provideService(GitHubCliAccountEnvironment, {
forCwd: () => Effect.succeed(config?.gitHubAccountEnvironment ?? {}),
}),
);
}),
).pipe(
Layer.provideMerge(
Expand Down Expand Up @@ -369,6 +375,29 @@ const sendCompletedClaudeTurn = (
});

describe("ClaudeAdapterLive", () => {
it.effect("starts queries as the checkout's selected GitHub CLI login", () => {
const harness = makeHarness({
environment: { ...process.env, GH_TOKEN: "ambient", GITHUB_TOKEN: "ambient" },
gitHubAccountEnvironment: { GH_TOKEN: "selected", GITHUB_TOKEN: "selected" },
});
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
runtimeMode: "full-access",
cwd: "/tmp/claude-github-login",
});
assert.include(harness.getLastCreateQueryInput()?.options.env, {
GH_TOKEN: "selected",
GITHUB_TOKEN: "selected",
});
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});

it.effect("returns validation error for non-claude provider on startSession", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down
8 changes: 7 additions & 1 deletion apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ import {
type ProviderAdapterError,
} from "../Errors.ts";
import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
import { GitHubCliAccountEnvironment } from "../../sourceControl/GitHubCli.ts";
import { spawnAndCollect } from "../providerSnapshot.ts";
import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.fromJsonString(Schema.Unknown));
Expand Down Expand Up @@ -2080,6 +2081,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, options?.environment).pipe(
Effect.provideService(Path.Path, path),
);
const gitHubAccountEnvironment = yield* GitHubCliAccountEnvironment;
const claudeSdkExecutablePath = yield* resolveClaudeSdkExecutablePath(
claudeSettings.binaryPath,
claudeEnvironment,
Expand Down Expand Up @@ -4890,6 +4892,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
extraArgs["thinking-display"] = "summarized";
}
const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);
const gitHubEnvironment = yield* gitHubAccountEnvironment.forCwd(input.cwd ?? process.cwd());
// The attachments dir grant lets the agent Read/copy pasted images at
// the paths ProviderService injects into the turn text, without an
// approval prompt. It is a leaf directory holding only attachment
Expand Down Expand Up @@ -4935,7 +4938,10 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
canUseTool,
onUserDialog,
supportedDialogKinds: ["resume_return"],
env: McpProviderSession.withAgentDeviceEnvironment(claudeEnvironment, mcpSession),
env: McpProviderSession.withAgentDeviceEnvironment(
{ ...claudeEnvironment, ...gitHubEnvironment },
mcpSession,
),
additionalDirectories,
...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}),
...(mcpSession
Expand Down
64 changes: 64 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as NodePath from "node:path";
import {
ApprovalRequestId,
CodexSettings,
EnvironmentId,
EventId,
ProviderDriverKind,
ProviderInstanceId,
Expand Down Expand Up @@ -36,6 +37,8 @@ import * as TestClock from "effect/testing/TestClock";
import * as CodexErrors from "effect-codex-app-server/errors";

import { ServerConfig } from "../../config.ts";
import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
import { GitHubCliAccountEnvironment } from "../../sourceControl/GitHubCli.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { ProviderAdapterValidationError } from "../Errors.ts";
import type { CodexAdapterShape } from "../Services/CodexAdapter.ts";
Expand Down Expand Up @@ -496,6 +499,67 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
}),
);

it.effect("starts app-servers as the checkout's selected GitHub CLI login", () => {
const runtimeFactory = makeRuntimeFactory();
const mcpThreadId = asThreadId("sess-github-login-mcp");
const layer = Layer.effect(
CodexAdapter,
makeCodexAdapter(decodeCodexSettings({}), {
makeRuntime: runtimeFactory.factory,
environment: { PATH: "/usr/bin", GH_TOKEN: "ambient", GITHUB_TOKEN: "ambient" },
}).pipe(
Effect.provideService(GitHubCliAccountEnvironment, {
forCwd: () => Effect.succeed({ GH_TOKEN: "selected", GITHUB_TOKEN: "selected" }),
}),
),
).pipe(
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(providerSessionDirectoryTestLayer),
Layer.provideMerge(NodeServices.layer),
);

return Effect.gen(function* () {
const adapter = yield* CodexAdapter;
yield* adapter.startSession({
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("sess-github-login"),
runtimeMode: "full-access",
});
NodeAssert.deepEqual(runtimeFactory.lastRuntime?.options.environment, {
PATH: "/usr/bin",
GH_TOKEN: "selected",
GITHUB_TOKEN: "selected",
});

// The device environment is layered over the selected login, not instead of it.
McpProviderSession.setMcpProviderSession({
environmentId: EnvironmentId.make("environment-1"),
threadId: mcpThreadId,
providerSessionId: "provider-session-1",
providerInstanceId: ProviderInstanceId.make("codex"),
endpoint: "http://127.0.0.1:1/mcp",
authorizationHeader: "Bearer mcp-token",
capabilities: new Set(["device"]),
agentDeviceEnvironment: { PATH: "/t3/device/bin", PATH_SEPARATOR: ":" },
});
yield* adapter.startSession({
provider: ProviderDriverKind.make("codex"),
threadId: mcpThreadId,
runtimeMode: "full-access",
});
NodeAssert.deepEqual(runtimeFactory.lastRuntime?.options.environment, {
PATH: "/t3/device/bin:/usr/bin",
GH_TOKEN: "selected",
GITHUB_TOKEN: "selected",
T3_MCP_BEARER_TOKEN: "mcp-token",
});
}).pipe(
Effect.ensuring(Effect.sync(() => McpProviderSession.clearMcpProviderSession(mcpThreadId))),
Effect.provide(layer),
);
});

it.effect("passes configured launch args into the session runtime", () => {
const runtimeFactory = makeRuntimeFactory();
const layer = Layer.effect(
Expand Down
15 changes: 12 additions & 3 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
type ProviderAdapterError,
} from "../Errors.ts";
import { type CodexAdapterShape } from "../Services/CodexAdapter.ts";
import { GitHubCliAccountEnvironment } from "../../sourceControl/GitHubCli.ts";
import { resolveAttachmentPath } from "../../attachmentStore.ts";
import { ServerConfig } from "../../config.ts";
import {
Expand Down Expand Up @@ -2241,6 +2242,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex");
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const crypto = yield* Crypto.Crypto;
const gitHubAccountEnvironment = yield* GitHubCliAccountEnvironment;
const serverConfig = yield* Effect.service(ServerConfig);
const nativeEventLogger =
options?.nativeEventLogger ??
Expand Down Expand Up @@ -2275,14 +2277,21 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
? getCodexServiceTierOptionValue(input.modelSelection)
: undefined;
const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);
const cwd = input.cwd ?? process.cwd();
const gitHubEnvironment = yield* gitHubAccountEnvironment.forCwd(cwd);
Comment thread
donnes marked this conversation as resolved.
// Undefined keeps the app-server inheriting the server's environment as-is.
const environment =
options?.environment || Object.keys(gitHubEnvironment).length > 0
? { ...(options?.environment ?? process.env), ...gitHubEnvironment }
: undefined;
const runtimeInput: CodexSessionRuntimeOptions = {
threadId: input.threadId,
providerInstanceId: boundInstanceId,
cwd: input.cwd ?? process.cwd(),
cwd,
binaryPath: codexConfig.binaryPath,
...(options?.models ? { models: options.models } : {}),
launchArgs: resolveCodexLaunchArgs(codexConfig.launchArgs, options?.environment),
...(options?.environment ? { environment: options.environment } : {}),
...(environment ? { environment } : {}),
...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}),
...(isCodexResumeCursorSchema(input.resumeCursor)
? { resumeCursor: input.resumeCursor }
Expand All @@ -2296,7 +2305,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
? {
environment: {
...McpProviderSession.withAgentDeviceEnvironment(
options?.environment ?? process.env,
environment ?? process.env,
mcpSession,
),
T3_MCP_BEARER_TOKEN: mcpSession.authorizationHeader.replace(/^Bearer\s+/, ""),
Expand Down
40 changes: 40 additions & 0 deletions apps/server/src/provider/Layers/CursorAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { ServerSettingsService } from "../../serverSettings.ts";
import type { CursorAdapterShape } from "../Services/CursorAdapter.ts";
import { makeCursorAdapter } from "./CursorAdapter.ts";
import { execScriptSource, writeFakeCli } from "../../testUtils/fakeCli.ts";
import { GitHubCliAccountEnvironment } from "../../sourceControl/GitHubCli.ts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
const decodeCursorSettings = Schema.decodeSync(CursorSettings);

Expand Down Expand Up @@ -162,6 +163,45 @@ const cursorAdapterTestLayer = it.layer(
);

cursorAdapterTestLayer("CursorAdapterLive", (it) => {
it.effect("starts the agent as the checkout's selected GitHub CLI login", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("cursor-github-login");
const dir = yield* Effect.promise(() =>
NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cursor-github-login-")),
);
const envLogPath = NodePath.join(dir, "env.json");
const wrapperPath = writeFakeCli({
directory: dir,
name: "fake-agent",
source: execScriptSource({
scriptPath: mockAgentPath,
envLog: { path: envLogPath, keys: ["GH_TOKEN", "GITHUB_TOKEN"] },
}),
});
const adapter = yield* makeCursorAdapter(decodeCursorSettings({ binaryPath: wrapperPath }), {
environment: { ...process.env, GH_TOKEN: "ambient", GITHUB_TOKEN: "ambient" },
}).pipe(
Effect.provideService(GitHubCliAccountEnvironment, {
forCwd: () => Effect.succeed({ GH_TOKEN: "selected", GITHUB_TOKEN: "selected" }),
}),
);
yield* adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" });
yield* adapter.stopSession(threadId);
const logged = yield* Effect.promise(() => NodeFSP.readFile(envLogPath, "utf8"));
assert.deepStrictEqual(
yield* Schema.decodeEffect(
Schema.fromJsonString(
Schema.Struct({ GH_TOKEN: Schema.String, GITHUB_TOKEN: Schema.String }),
),
)(logged),
{
GH_TOKEN: "selected",
GITHUB_TOKEN: "selected",
},
);
}),
);

it.effect("rejects rollback without discarding the provider conversation", () =>
Effect.gen(function* () {
const adapter = yield* CursorAdapter;
Expand Down
Loading
Loading