Skip to content

Commit 8f98eda

Browse files
committed
fix: harness routing, codex exec, scheduled tasks UI
Made-with: Cursor
1 parent b22021c commit 8f98eda

27 files changed

Lines changed: 1458 additions & 287 deletions

apps/server/src/git/Layers/CodexTextGeneration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {
165165
codexSettings?.binaryPath || "codex",
166166
[
167167
"exec",
168+
"--skip-git-repo-check",
168169
"--ephemeral",
169170
"-s",
170171
"read-only",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import assert from "node:assert";
2+
import { afterEach, describe, it } from "vitest";
3+
4+
import {
5+
isClaudeSubscriptionOAuthTokenConfigured,
6+
probeClaudeSubscriptionAuth,
7+
} from "./cliAuthProbe.ts";
8+
9+
describe("cliAuthProbe", () => {
10+
afterEach(() => {
11+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
12+
});
13+
14+
it("isClaudeSubscriptionOAuthTokenConfigured is false when unset", () => {
15+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
16+
assert.strictEqual(isClaudeSubscriptionOAuthTokenConfigured(), false);
17+
});
18+
19+
it("isClaudeSubscriptionOAuthTokenConfigured is false for whitespace", () => {
20+
process.env.CLAUDE_CODE_OAUTH_TOKEN = " \n\t ";
21+
assert.strictEqual(isClaudeSubscriptionOAuthTokenConfigured(), false);
22+
});
23+
24+
it("isClaudeSubscriptionOAuthTokenConfigured is true when token non-empty", () => {
25+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "sk-ant-oat01-test";
26+
assert.strictEqual(isClaudeSubscriptionOAuthTokenConfigured(), true);
27+
});
28+
29+
it("probeClaudeSubscriptionAuth resolves true when OAuth token env is set", async () => {
30+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "sk-ant-oat01-test";
31+
assert.strictEqual(await probeClaudeSubscriptionAuth(), true);
32+
});
33+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { execFile } from "node:child_process";
2+
import { promisify } from "node:util";
3+
4+
import type { CommandResult } from "../provider/providerSnapshot.ts";
5+
import { parseClaudeAuthStatusFromOutput } from "../provider/Layers/ClaudeProvider.ts";
6+
import { parseAuthStatusFromOutput } from "../provider/Layers/CodexProvider.ts";
7+
8+
const execFileAsync = promisify(execFile);
9+
10+
export function isClaudeSubscriptionOAuthTokenConfigured(): boolean {
11+
const raw = process.env.CLAUDE_CODE_OAUTH_TOKEN;
12+
if (typeof raw !== "string") {
13+
return false;
14+
}
15+
return raw.trim().length > 0;
16+
}
17+
18+
async function execFileResult(
19+
file: string,
20+
args: readonly string[],
21+
env?: NodeJS.ProcessEnv,
22+
): Promise<CommandResult> {
23+
try {
24+
const r = await execFileAsync(file, [...args], {
25+
encoding: "utf8",
26+
maxBuffer: 4 * 1024 * 1024,
27+
timeout: 12_000,
28+
...(env ? { env } : {}),
29+
});
30+
return {
31+
stdout: String(r.stdout ?? ""),
32+
stderr: String(r.stderr ?? ""),
33+
code: 0,
34+
};
35+
} catch (cause: unknown) {
36+
const err = cause as {
37+
stdout?: string | Buffer;
38+
stderr?: string | Buffer;
39+
code?: number;
40+
};
41+
return {
42+
stdout: err.stdout !== undefined ? String(err.stdout) : "",
43+
stderr: err.stderr !== undefined ? String(err.stderr) : "",
44+
code: typeof err.code === "number" ? err.code : 1,
45+
};
46+
}
47+
}
48+
49+
export async function probeClaudeSubscriptionAuth(binaryPath?: string): Promise<boolean> {
50+
if (isClaudeSubscriptionOAuthTokenConfigured()) {
51+
return true;
52+
}
53+
const bin = binaryPath?.trim() || "claude";
54+
const result = await execFileResult(bin, ["auth", "status"]);
55+
const parsed = parseClaudeAuthStatusFromOutput(result);
56+
return parsed.status === "ready" && parsed.auth.status === "authenticated";
57+
}
58+
59+
export async function probeCodexSubscriptionAuth(
60+
binaryPath?: string,
61+
homePath?: string,
62+
): Promise<boolean> {
63+
const bin = binaryPath?.trim() || "codex";
64+
const env =
65+
homePath && homePath.trim().length > 0
66+
? { ...process.env, CODEX_HOME: homePath.trim() }
67+
: process.env;
68+
const result = await execFileResult(bin, ["login", "status"], env);
69+
const parsed = parseAuthStatusFromOutput(result);
70+
return parsed.status === "ready" && parsed.auth.status === "authenticated";
71+
}

apps/server/src/harness/engine/loop.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import {
2626
} from "../tools/scheduledTasks";
2727
import { streamAnthropic } from "../providers/anthropic";
2828
import { streamOpenAI } from "../providers/openai";
29+
import {
30+
runClaudeSubscriptionTurn,
31+
runCodexSubscriptionTurn,
32+
} from "../providers/cliSubscriptionTurn";
2933
import { buildSystemPrompt } from "./prompt";
3034
import { McpManager, type McpServerConfig } from "../mcp/client";
3135
import { LspManager } from "../lsp/client";
@@ -113,6 +117,52 @@ export async function* runAgentLoop(options: AgentLoopOptions): AsyncGenerator<A
113117
const messages: ConversationMessage[] = [...conversationHistory];
114118
messages.push({ role: "user", content: userMessage });
115119

120+
if (config.provider === "anthropic" && config.upstream.kind === "claude_subscription") {
121+
for await (const event of runClaudeSubscriptionTurn({
122+
model: config.model,
123+
cwd: config.workspaceRoot,
124+
systemPrompt,
125+
conversationHistory,
126+
userMessage,
127+
signal,
128+
claudeBinaryPath: config.upstream.claudeBinaryPath,
129+
mode: config.mode,
130+
harnessRuntimeMode: config.harnessRuntimeMode ?? "auto-accept-edits",
131+
})) {
132+
yield event;
133+
}
134+
await mcpManager.closeAll();
135+
return;
136+
}
137+
138+
if (config.provider === "openai" && config.upstream.kind === "openai_subscription") {
139+
for await (const event of runCodexSubscriptionTurn({
140+
model: config.model,
141+
cwd: config.workspaceRoot,
142+
systemPrompt,
143+
conversationHistory,
144+
userMessage,
145+
signal,
146+
codexBinaryPath: config.upstream.codexBinaryPath,
147+
codexHomePath: config.upstream.codexHomePath,
148+
})) {
149+
yield event;
150+
}
151+
await mcpManager.closeAll();
152+
return;
153+
}
154+
155+
if (config.upstream.kind !== "api_key") {
156+
yield {
157+
type: "error",
158+
error: "Harness API routing needs an API key for this model provider.",
159+
};
160+
await mcpManager.closeAll();
161+
return;
162+
}
163+
164+
const apiUpstream = config.upstream;
165+
116166
let turnNumber = 0;
117167
const editedFiles: Set<string> = new Set();
118168
let nudgeState = createNudgeState();
@@ -134,8 +184,8 @@ export async function* runAgentLoop(options: AgentLoopOptions): AsyncGenerator<A
134184
config.provider === "anthropic"
135185
? streamAnthropic({
136186
model: config.model,
137-
apiKey: config.apiKey,
138-
...(config.baseURL ? { baseURL: config.baseURL } : {}),
187+
apiKey: apiUpstream.apiKey,
188+
...(apiUpstream.baseURL ? { baseURL: apiUpstream.baseURL } : {}),
139189
messages,
140190
tools: allTools,
141191
systemPrompt,
@@ -144,9 +194,9 @@ export async function* runAgentLoop(options: AgentLoopOptions): AsyncGenerator<A
144194
})
145195
: streamOpenAI({
146196
model: config.model,
147-
apiKey: config.apiKey,
197+
apiKey: apiUpstream.apiKey,
148198
baseURL:
149-
config.baseURL ??
199+
apiUpstream.baseURL ??
150200
(config.provider === "openrouter"
151201
? "https://openrouter.ai/api/v1"
152202
: "https://api.openai.com/v1"),

0 commit comments

Comments
 (0)