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
50 changes: 45 additions & 5 deletions apps/server/src/automation/AutomationService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import {
} from "@t3tools/contracts";
import * as DateTime from "effect/DateTime";
import * as Context from "effect/Context";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Queue from "effect/Queue";
import * as Redacted from "effect/Redacted";
import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";
import type * as Scope from "effect/Scope";
Expand All @@ -37,6 +39,9 @@ import * as ThreadManagementService from "../orchestration-v2/ThreadManagementSe
import * as ProjectService from "../project/ProjectService.ts";
import * as ProviderRegistry from "../provider/ProviderRegistry.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as GitHubApi from "../sourceControl/GitHubApi.ts";
import { GitHubCliAccountSelection } from "../sourceControl/GitHubCliAccountSelection.ts";
import * as GitHubCredentials from "../sourceControl/GitHubCredentials.ts";
import * as VcsProcess from "../vcs/VcsProcess.ts";
import * as TextGeneration from "../textGeneration/TextGeneration.ts";
import {
Expand Down Expand Up @@ -66,6 +71,12 @@ const CONFIG: AutomationConfig = {
};

/** What a test can steer and observe around the service. */
/** Answers the service's `gh` calls; `env` carries the token it passed, if any. */
type GhResponder = (
args: ReadonlyArray<string>,
env: NodeJS.ProcessEnv | undefined,
) => Effect.Effect<string>;

interface Harness {
/** Runs once, just before the first `thread.delete` reaches the orchestrator. */
readonly beforeDelete: Ref.Ref<Effect.Effect<void> | null>;
Expand All @@ -76,7 +87,7 @@ interface Harness {
readonly summaries: Queue.Queue<ReadonlyArray<string>>;
/** Effect ids the service read from the outbox. */
readonly outboxReads: Queue.Queue<string>;
readonly gh: Ref.Ref<(args: ReadonlyArray<string>) => Effect.Effect<string>>;
readonly gh: Ref.Ref<GhResponder>;
readonly worktrees: Ref.Ref<{
readonly created: ReadonlyArray<string>;
readonly removed: ReadonlyArray<string>;
Expand Down Expand Up @@ -226,11 +237,18 @@ const environmentLayer = (harness: Harness) =>
Layer.mock(VcsProcess.VcsProcess)({
run: (input) =>
Ref.get(harness.gh).pipe(
Effect.flatMap((respond) => respond(input.args)),
Effect.flatMap((respond) => respond(input.args, input.env)),
Effect.map((stdout) => ({ stdout, stderr: "", exitCode: 0 }) as never),
),
}),
Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
Layer.mock(GitHubApi.GitHubApi)({
credential: () =>
Effect.map(GitHubCredentials.ProfileGitHubAccount, (account) => ({
token: Redacted.make(`token-for-${account?.login ?? "active"}`),
fingerprint: "test",
})),
}),
Layer.mock(TextGeneration.TextGeneration)({
generateRunSummary: (request) =>
Queue.offer(harness.summaries, request.agentMessages).pipe(
Expand Down Expand Up @@ -272,9 +290,7 @@ const withHarness = <A, E>(body: (harness: Harness) => Effect.Effect<A, E, Env |
launches: yield* Ref.make<ReadonlyArray<ThreadLaunchService.ThreadLaunchInput>>([]),
summaries: yield* Queue.unbounded<ReadonlyArray<string>>(),
outboxReads: yield* Queue.unbounded<string>(),
gh: yield* Ref.make<(args: ReadonlyArray<string>) => Effect.Effect<string>>(() =>
Effect.succeed("[]"),
),
gh: yield* Ref.make<GhResponder>(() => Effect.succeed("[]")),
worktrees: yield* Ref.make<{
readonly created: ReadonlyArray<string>;
readonly removed: ReadonlyArray<string>;
Expand Down Expand Up @@ -945,6 +961,30 @@ it.effect("reviews a new pull request detached at its head and links the thread"
),
);

it.effect("polls GitHub as the login the project's profile picks", () =>
withHarness((harness) =>
Effect.gen(function* () {
const listToken = yield* Deferred.make<string | undefined>();
yield* Ref.set(harness.gh, (args, env) =>
args[0] === "pr" && args[1] === "list"
? Deferred.succeed(listToken, env?.GH_TOKEN).pipe(Effect.as("[]"))
: Effect.succeed("[]"),
);
yield* createWatching(harness, {
...CONFIG,
triggers: [{ type: "github", event: "pull_request.opened" }],
});
yield* startService(harness).pipe(
Effect.provideService(GitHubCliAccountSelection, {
forCwd: (cwd) =>
Effect.succeed(cwd === WORKSPACE_ROOT ? { host: "github.com", login: "work" } : null),
}),
);
assert.strictEqual(yield* Deferred.await(listToken), "token-for-work");
}),
),
);

it.effect("removes the pull request worktree and records the failure when checkout fails", () =>
withHarness((harness) =>
Effect.gen(function* () {
Expand Down
26 changes: 23 additions & 3 deletions apps/server/src/automation/AutomationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Redacted from "effect/Redacted";
import * as Schedule from "effect/Schedule";
import * as Schema from "effect/Schema";
import type * as Scope from "effect/Scope";
Expand All @@ -71,10 +72,20 @@ import * as ProviderRegistry from "../provider/ProviderRegistry.ts";
import { forkParked } from "../serverActivation.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as VcsProcess from "../vcs/VcsProcess.ts";
import { GitHubCliAccountEnvironment } from "../sourceControl/GitHubCliAccountSelection.ts";
import * as GitHubApi from "../sourceControl/GitHubApi.ts";
import { GitHubCliAccountSelection } from "../sourceControl/GitHubCliAccountSelection.ts";
import * as GitHubCredentials from "../sourceControl/GitHubCredentials.ts";
import * as TextGeneration from "../textGeneration/TextGeneration.ts";
import { RUN_SUMMARY_TRANSCRIPT_MAX_LENGTH } from "../textGeneration/TextGenerationPrompts.ts";

/** gh reads github.com and GHE.com tenancies from GH_TOKEN, every other host from GH_ENTERPRISE_TOKEN. */
function ghTokenEnvironment(host: string, token: string): Record<string, string> {
const normalized = host.toLowerCase();
return normalized === "github.com" || normalized.endsWith(".ghe.com")
? { GH_TOKEN: token }
: { GH_ENTERPRISE_TOKEN: token };
}

/** A run that has not ended yet; `messageAt` is when the run's message was sent. */
const PendingRun = Schema.Struct({
/** Absent, with `messageId`, on entries saved before runs kept summaries. */
Expand Down Expand Up @@ -350,14 +361,23 @@ const make = Effect.gen(function* () {
const gitWorkflow = yield* GitWorkflowService.GitWorkflowService;
const settingsService = yield* ServerSettings.ServerSettingsService;
const process = yield* VcsProcess.VcsProcess;
const accountEnvironment = yield* GitHubCliAccountEnvironment;
const gitHubAccounts = yield* GitHubCliAccountSelection;
const gitHubApi = yield* GitHubApi.GitHubApi;
/** Runs `gh` with the token of the login the project's profile picks, when it picks one. */
const gitHubCli = {
execute: Effect.fn(function* (input: {
cwd: string;
args: readonly string[];
timeoutMs?: number;
}) {
const env = yield* accountEnvironment.forCwd(input.cwd);
const account = yield* gitHubAccounts.forCwd(input.cwd);
const env =
account === null
? {}
: yield* gitHubApi.credential(account.host).pipe(
Effect.provideService(GitHubCredentials.ProfileGitHubAccount, account),
Effect.map(({ token }) => ghTokenEnvironment(account.host, Redacted.value(token))),
);
return yield* process.run({
...input,
env,
Expand Down
2 changes: 0 additions & 2 deletions apps/server/src/provider/Drivers/AcpRegistryDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
} from "../../orchestration-v2/Adapters/AcpRegistryAdapterV2.ts";
import * as ServerSettings from "../../serverSettings.ts";
import type { TextGeneration } from "../../textGeneration/TextGeneration.ts";
import { withGitHubAccountSpawner } from "../ProviderGitHubAccountEnvironment.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
Expand Down Expand Up @@ -516,7 +515,6 @@ export const AcpRegistryDriver: ProviderDriver<AcpRegistrySettings, AcpRegistryD
enabled,
config,
}).pipe(
withGitHubAccountSpawner,
Effect.mapError(
(cause) =>
new ProviderDriverError({
Expand Down
38 changes: 0 additions & 38 deletions apps/server/src/provider/Drivers/AntigravityDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import {
ANTIGRAVITY_DEFAULT_MODEL,
ProviderInstanceId,
type AntigravitySettings,
ProviderSessionId,
ThreadId,
} from "@t3tools/contracts";
import {
HostProcessEnvironment,
Expand All @@ -25,9 +23,7 @@ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import * as ServerConfig from "../../config.ts";
import { ProviderAdapterV2RuntimePolicy } from "../../orchestration-v2/ProviderAdapter.ts";
import * as ServerSettings from "../../serverSettings.ts";
import { GitHubCliAccountEnvironment } from "../../sourceControl/GitHubCliAccountSelection.ts";
import * as AntigravityInstallation from "../AntigravityInstallation.ts";
import {
ANTIGRAVITY_AUTH_STDOUT_PREFIX,
Expand Down Expand Up @@ -127,7 +123,6 @@ 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 @@ -181,7 +176,6 @@ 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 @@ -286,38 +280,6 @@ it.layer(layerTest)("AntigravityDriver", (it) => {
),
);

it.effect.skipIf(windowsHost)(
"starts session agents as the checkout's selected GitHub CLI login",
() =>
Effect.gen(function* () {
const checkout = process.cwd();
const h = yield* makeHarness({ enabled: true }).pipe(
Effect.provideService(GitHubCliAccountEnvironment, {
forCwd: (cwd) =>
Effect.succeed(
cwd === checkout ? { GH_TOKEN: "selected", GITHUB_TOKEN: "selected" } : {},
),
}),
);
const threadId = ThreadId.make("antigravity-github-login");
const modelSelection = { instanceId: h.instance.instanceId, model: "gemini-test-low" };
const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd: checkout,
});
const session = yield* h.instance.orchestrationAdapter.openSession({
threadId,
providerSessionId: ProviderSessionId.make("provider-session-antigravity-github-login"),
modelSelection,
runtimePolicy,
});
yield* session.ensureThread({ threadId, modelSelection, runtimePolicy });
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
8 changes: 1 addition & 7 deletions apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { withAgentDeviceEnvironment } from "../../mcp/McpProviderSession.ts";
import { GitHubCliAccountEnvironment } from "../../sourceControl/GitHubCliAccountSelection.ts";
import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
Expand Down Expand Up @@ -94,8 +93,6 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
// Session agents start as their checkout's selected `gh` login.
const gitHubAccounts = yield* GitHubCliAccountEnvironment;
const serverConfig = yield* ServerConfig.ServerConfig;
const selfInvocation = yield* resolveSelfInvocation();
const installation = yield* AntigravityInstallation.AntigravityInstallation;
Expand Down Expand Up @@ -245,10 +242,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
installation: executable,
profile,
cwd: input.cwd,
baseEnv: withAgentDeviceEnvironment(
{ ...processEnvironment, ...(yield* gitHubAccounts.forCwd(input.cwd)) },
input,
),
baseEnv: withAgentDeviceEnvironment(processEnvironment, input),
auth,
runtimeTempDirectory,
}),
Expand Down
7 changes: 1 addition & 6 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ import {
type ClaudeAdapterV2DriverEnv,
} from "../../orchestration-v2/Adapters/ClaudeAdapterV2.ts";
import * as ServerSettings from "../../serverSettings.ts";
import {
withGitHubAccountClaudeQueries,
withGitHubAccountSpawner,
} from "../ProviderGitHubAccountEnvironment.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeClaudeScopedLimitNames } from "../claudeUsageLimits.ts";
import * as ClaudeResetCredits from "../claudeResetCredits.ts";
Expand Down Expand Up @@ -184,7 +180,6 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
},
{ scopedLimitNames, onUsageLimits: (update) => snapshot.applyUsageLimits(update) },
).pipe(
withGitHubAccountClaudeQueries,
Effect.mapError(
(cause) =>
new ProviderDriverError({
Expand All @@ -199,7 +194,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
effectiveConfig,
processEnv,
modelCatalog,
).pipe(withGitHubAccountSpawner);
);

// Per-instance capabilities cache: keyed on binary + resolved HOME so
// account-specific probes never share auth metadata across instances.
Expand Down
Loading
Loading