fix: run AgentOS workspaces on remote runner

This commit is contained in:
-Puter
2026-07-28 20:08:46 +05:30
parent 092a9793ea
commit 0d7162544b
7 changed files with 335 additions and 211 deletions

View File

@@ -21,6 +21,7 @@
"@code/primitives": "workspace:*",
"@flue/runtime": "latest",
"@rivet-dev/agentos": "0.2.14",
"@rivet-dev/agentos-core": "0.2.14",
"convex": "catalog:",
"hono": "catalog:",
"rivetkit": "2.3.9",

View File

@@ -1,7 +1,6 @@
import { Resolver } from "node:dns/promises";
import { parseAgentEnv } from "@code/env/agent";
import { agentOS, setup } from "@rivet-dev/agentos";
import { createHostDirBackend } from "@rivet-dev/agentos-core";
import { createClient } from "@rivet-dev/agentos/client";
import { Effect } from "effect";
@@ -17,6 +16,7 @@ import type {
ExecutionEvent,
WorkAttemptExecutionResult,
} from "../../../primitives/src/execution-runtime";
import { HostRepositoryWorkspace } from "./host-repository";
const codexConfig = makeCodexAgentOsConfig();
const workspace = agentOS<undefined, { token: string }>({
@@ -34,9 +34,6 @@ const workspace = agentOS<undefined, { token: string }>({
export const runtimeRegistry = setup({ use: { workspace } });
const shellQuote = (value: string): string =>
`'${value.replaceAll("'", `'"'"'`)}'`;
const event = (
sequence: number,
kind: ExecutionEvent["kind"],
@@ -50,16 +47,6 @@ const event = (
sequence,
});
const requireSuccess = (
result: { exitCode: number; stderr: string; stdout: string },
operation: string
) => {
if (result.exitCode !== 0) {
throw new Error(`${operation} failed: ${result.stderr || result.stdout}`);
}
return result.stdout.trim();
};
const executionError = (
message: string,
reason: WorkAttemptExecutionError["reason"],
@@ -86,33 +73,6 @@ const classifyRuntimeFailure = (cause: unknown): WorkAttemptExecutionError => {
return executionError(message, "HarnessFailed", false);
};
const gitAuthEnv = (
username: string,
credential: string,
repositoryUrl: string,
repositoryAddress: string
) => {
const { hostname } = new URL(repositoryUrl);
return {
GIT_CONFIG_COUNT: "2",
GIT_CONFIG_KEY_0: "http.extraHeader",
GIT_CONFIG_KEY_1: "http.curloptResolve",
GIT_CONFIG_VALUE_0: `Authorization: Basic ${Buffer.from(`${username}:${credential}`).toString("base64")}`,
GIT_CONFIG_VALUE_1: `${hostname}:443:${repositoryAddress}`,
};
};
const resolveRepositoryAddress = async (repositoryUrl: string) => {
const { hostname } = new URL(repositoryUrl);
const resolver = new Resolver();
resolver.setServers(["1.1.1.1", "8.8.8.8"]);
const [address] = await resolver.resolve4(hostname);
if (!address) {
throw new Error(`Repository host ${hostname} did not resolve`);
}
return address;
};
export const executeAgentOsAttempt = async (
rawInput: unknown
): Promise<WorkAttemptExecutionResult> => {
@@ -133,39 +93,26 @@ export const executeAgentOsAttempt = async (
workspaceKey: input.workspaceKey,
}),
];
const authEnv = gitAuthEnv(
input.auth.username ??
(input.auth.provider === "github" ? "x-access-token" : "git"),
input.auth.credential,
input.repositoryUrl,
await resolveRepositoryAddress(input.repositoryUrl)
const hostRepository = new HostRepositoryWorkspace();
const prepared = await hostRepository.prepare(input);
events.push(
event(
1,
prepared.cloned ? "repository.cloning" : "runtime.preparing",
prepared.cloned
? "Project repository cloned on the execution host"
: "Host repository checkout reused"
)
);
await vm.mkdir("/workspace", { recursive: true });
if (!(await vm.exists("/workspace/repository/.git"))) {
events.push(event(1, "repository.cloning", "Cloning project repository"));
const clone = await vm.execArgv(
"git",
["clone", input.repositoryUrl, "/workspace/repository"],
{
cwd: "/workspace",
env: authEnv,
}
);
requireSuccess(clone, "Repository clone");
}
const checkout = await vm.exec(
`git fetch origin ${shellQuote(input.baseBranch)} && git checkout -B ${shellQuote(`zopu/${input.runId}`)} ${shellQuote(`origin/${input.baseBranch}`)}`,
{ cwd: "/workspace/repository", env: authEnv }
);
requireSuccess(checkout, "Repository checkout");
const baseRevision = requireSuccess(
await vm.execArgv("git", ["rev-parse", "HEAD"], {
cwd: "/workspace/repository",
await vm.mountFs({
path: "/workspace/repository",
plugin: createHostDirBackend({
hostPath: prepared.checkoutPath,
readOnly: false,
}),
"Base revision lookup"
);
readOnly: false,
});
const { baseRevision } = prepared;
events.push(
event(2, "repository.ready", "Repository checkout is ready", {
baseRevision,
@@ -210,56 +157,12 @@ export const executeAgentOsAttempt = async (
})
);
const status = requireSuccess(
await vm.execArgv("git", ["status", "--porcelain"], {
cwd: "/workspace/repository",
}),
"Changed file lookup"
);
const diff = requireSuccess(
await vm.execArgv("git", ["diff", "--binary", "HEAD"], {
cwd: "/workspace/repository",
}),
"Diff collection"
);
const changedFiles = status
.split("\n")
.filter(Boolean)
.map((line) => line.slice(3).trim());
let candidateRevision = baseRevision;
if (changedFiles.length > 0) {
requireSuccess(
await vm.execArgv("git", ["add", "-A"], {
cwd: "/workspace/repository",
}),
"Candidate staging"
);
const tree = requireSuccess(
await vm.execArgv("git", ["write-tree"], {
cwd: "/workspace/repository",
}),
"Candidate tree creation"
);
candidateRevision = requireSuccess(
await vm.exec(
`printf %s ${shellQuote(`Zopu candidate for ${input.attemptId}`)} | git commit-tree ${shellQuote(tree)} -p ${shellQuote(baseRevision)}`,
{
cwd: "/workspace/repository",
env: {
GIT_AUTHOR_EMAIL: "agent@zopu.dev",
GIT_AUTHOR_NAME: "Zopu Agent",
GIT_COMMITTER_EMAIL: "agent@zopu.dev",
GIT_COMMITTER_NAME: "Zopu Agent",
},
}
),
"Candidate revision creation"
);
requireSuccess(
await vm.execArgv("git", ["reset"], { cwd: "/workspace/repository" }),
"Candidate index reset"
);
}
const collected = await HostRepositoryWorkspace.collect({
attemptId: input.attemptId,
baseRevision,
checkoutPath: prepared.checkoutPath,
});
const { candidateRevision, changedFiles, diff } = collected;
events.push(
event(
5,

View File

@@ -0,0 +1,232 @@
import { createHash } from "node:crypto";
import { mkdir } from "node:fs/promises";
import path from "node:path";
import type { RepositoryExecutionAuth } from "../../../primitives/src/execution-runtime";
interface PrepareRepositoryInput {
auth: Pick<RepositoryExecutionAuth, "credential" | "provider" | "username">;
baseBranch: string;
repositoryUrl: string;
runId: string;
workspaceKey: string;
}
interface PreparedRepository {
baseRevision: string;
checkoutPath: string;
cloned: boolean;
}
interface CollectRepositoryInput {
attemptId: string;
baseRevision: string;
checkoutPath: string;
}
interface CollectedRepository {
candidateRevision: string;
changedFiles: string[];
diff: string;
}
interface GitResult {
exitCode: number;
stderr: string;
stdout: string;
}
const requireSuccess = (result: GitResult, operation: string): string => {
if (result.exitCode !== 0) {
throw new Error(`${operation} failed: ${result.stderr || result.stdout}`);
}
return result.stdout.trim();
};
const authEnvironment = (
auth: PrepareRepositoryInput["auth"]
): Record<string, string> => ({
GIT_CONFIG_COUNT: "1",
GIT_CONFIG_KEY_0: "http.extraHeader",
GIT_CONFIG_VALUE_0: `Authorization: Basic ${Buffer.from(
`${auth.username ?? (auth.provider === "github" ? "x-access-token" : "git")}:${auth.credential}`
).toString("base64")}`,
GIT_TERMINAL_PROMPT: "0",
});
const changedFilePath = (line: string): string => {
const filePath = line.slice(3).trim();
const renameSeparator = " -> ";
const renameIndex = filePath.lastIndexOf(renameSeparator);
return renameIndex === -1
? filePath
: filePath.slice(renameIndex + renameSeparator.length);
};
const runGit = async (
cwd: string,
args: readonly string[],
env: Record<string, string> = {}
): Promise<GitResult> => {
const environment = Object.fromEntries(
Object.entries(process.env).filter(
(entry): entry is [string, string] => entry[1] !== undefined
)
);
const child = Bun.spawn(["git", ...args], {
cwd,
env: { ...environment, ...env },
stderr: "pipe",
stdout: "pipe",
});
const [exitCode, stderr, stdout] = await Promise.all([
child.exited,
new Response(child.stderr).text(),
new Response(child.stdout).text(),
]);
return { exitCode, stderr, stdout };
};
export class HostRepositoryWorkspace {
readonly #root: string;
constructor(
root = process.env.AGENT_WORKSPACE_ROOT ?? "/var/lib/zopu/workspaces"
) {
this.#root = root;
}
async prepare(input: PrepareRepositoryInput): Promise<PreparedRepository> {
const checkoutPath = path.join(
this.#root,
createHash("sha256")
.update(input.workspaceKey)
.digest("hex")
.slice(0, 32),
"repository"
);
await mkdir(path.dirname(checkoutPath), { recursive: true });
const cloned = !(await Bun.file(
path.join(checkoutPath, ".git", "HEAD")
).exists());
const authEnv = authEnvironment(input.auth);
if (cloned) {
requireSuccess(
await runGit(
this.#root,
["clone", input.repositoryUrl, checkoutPath],
authEnv
),
"Repository clone"
);
} else {
requireSuccess(
await runGit(checkoutPath, [
"remote",
"set-url",
"origin",
input.repositoryUrl,
]),
"Repository remote update"
);
}
requireSuccess(
await runGit(
checkoutPath,
["fetch", "origin", input.baseBranch],
authEnv
),
"Repository fetch"
);
requireSuccess(
await runGit(checkoutPath, [
"checkout",
"-B",
`zopu/${input.runId}`,
`origin/${input.baseBranch}`,
]),
"Repository checkout"
);
requireSuccess(
await runGit(checkoutPath, ["reset", "--hard", "HEAD"]),
"Repository reset"
);
requireSuccess(
await runGit(checkoutPath, ["clean", "-fdx"]),
"Repository clean"
);
return {
baseRevision: requireSuccess(
await runGit(checkoutPath, ["rev-parse", "HEAD"]),
"Base revision lookup"
),
checkoutPath,
cloned,
};
}
static async collect(
input: CollectRepositoryInput
): Promise<CollectedRepository> {
const status = requireSuccess(
await runGit(input.checkoutPath, ["status", "--porcelain"]),
"Changed file lookup"
);
let diff = "";
const changedFiles = status
.split("\n")
.filter(Boolean)
.map(changedFilePath);
let candidateRevision = input.baseRevision;
if (changedFiles.length > 0) {
requireSuccess(
await runGit(input.checkoutPath, ["add", "-A"]),
"Candidate staging"
);
diff = requireSuccess(
await runGit(input.checkoutPath, [
"diff",
"--binary",
"--cached",
"--no-ext-diff",
input.baseRevision,
]),
"Diff collection"
);
const tree = requireSuccess(
await runGit(input.checkoutPath, ["write-tree"]),
"Candidate tree creation"
);
candidateRevision = requireSuccess(
await runGit(
input.checkoutPath,
[
"commit-tree",
tree,
"-p",
input.baseRevision,
"-m",
`Zopu candidate for ${input.attemptId}`,
],
{
GIT_AUTHOR_EMAIL: "agent@zopu.dev",
GIT_AUTHOR_NAME: "Zopu Agent",
GIT_COMMITTER_EMAIL: "agent@zopu.dev",
GIT_COMMITTER_NAME: "Zopu Agent",
}
),
"Candidate revision creation"
);
requireSuccess(
await runGit(input.checkoutPath, ["reset"]),
"Candidate index reset"
);
}
return { candidateRevision, changedFiles, diff };
}
}

View File

@@ -30,7 +30,7 @@
"test:watch": "vitest"
},
"dependencies": {
"@agentos-software/codex": "0.2.7",
"@agentos-software/codex": "0.0.0-agent-acp-codex-session-benchmark.fd745e7",
"@agentos-software/git": "0.3.3",
"@rivet-dev/agentos": "0.2.14",
"effect": "catalog:"

View File

@@ -84,7 +84,7 @@ export const createAgentOsActorEffect = Effect.fn("createAgentOsActorEffect")(
// ---------------------------------------------------------------------------
/** Software bundle for a Codex-capable VM: the Codex harness plus git. */
export const codexSoftware = [codex, getExecutableGitSoftware()] as const;
export const codexSoftware = [...codex, getExecutableGitSoftware()] as const;
/** Model-provider env that Codex reads inside the VM. Pass this to the session's
* `env` when opening a Codex session (`agent: "codex"`). */
@@ -99,7 +99,9 @@ export const codexSessionEnv = (
model: CodexModelEnv,
extra: Readonly<Record<string, string>> = {}
): Record<string, string> => ({
CODEX_HOME: "/tmp/codex-home",
CODEX_MODEL: model.model,
HOME: "/tmp",
OPENAI_API_KEY: model.apiKey,
OPENAI_BASE_URL: model.baseUrl,
...extra,