From ffecff3857669a4ca12c84fd0d9eedb3c34ddd37 Mon Sep 17 00:00:00 2001 From: -Puter <22245429+puterhimself@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:20:42 +0530 Subject: [PATCH] Run fixed Zopu worktrees with Pi --- bun.lock | 19 +- deploy/zopu-runtime/.env.template | 21 +- deploy/zopu-runtime/runner/Dockerfile | 2 +- deploy/zopu-runtime/runner/docker-compose.yml | 4 +- packages/agents/package.json | 1 - packages/agents/src/runtime/agent-os.ts | 95 +++++--- .../agents/src/runtime/host-repository.ts | 217 +++++++++++------- packages/backend/convex/workExecutionAgent.ts | 2 +- .../backend/convex/workExecutionWorkflow.ts | 2 +- packages/env/src/agent.ts | 1 + packages/primitives/package.json | 2 +- packages/primitives/src/agent-os.test.ts | 34 --- packages/primitives/src/agent-os.ts | 97 +++++--- 13 files changed, 281 insertions(+), 216 deletions(-) delete mode 100644 packages/primitives/src/agent-os.test.ts diff --git a/bun.lock b/bun.lock index b559f74..61eddb9 100644 --- a/bun.lock +++ b/bun.lock @@ -64,7 +64,6 @@ "name": "@code/agents", "version": "0.0.0", "dependencies": { - "@agentos-software/codex-cli": "0.3.4", "@agentos-software/git": "0.3.3", "@code/backend": "workspace:*", "@code/env": "workspace:*", @@ -155,8 +154,8 @@ "name": "@code/primitives", "version": "0.0.0", "dependencies": { - "@agentos-software/codex": "0.0.0-agent-acp-codex-session-benchmark.fd745e7", "@agentos-software/git": "0.3.3", + "@agentos-software/pi": "0.2.7", "@rivet-dev/agentos": "0.2.14", "effect": "catalog:", }, @@ -247,8 +246,6 @@ "@agentos-software/claude-code": ["@agentos-software/claude-code@0.2.7", "", { "dependencies": { "@agentclientprotocol/sdk": "^0.16.1", "@anthropic-ai/claude-agent-sdk": "0.2.87", "zod": "^4.1.11" }, "bin": { "claude": "dist/claude-cli.mjs", "claude-sdk-acp": "dist/adapter.js" } }, "sha512-gXmqqOUWT98QvLkQXHn1UU8OOvqMO8BRSj+0PT99mOL6XNpBlPW6L065FJ9dC4IwJC5xeGzB1XFevjOdP7LwQg=="], - "@agentos-software/codex": ["@agentos-software/codex@0.0.0-agent-acp-codex-session-benchmark.fd745e7", "", { "dependencies": { "@agentclientprotocol/sdk": "^0.16.1", "@agentos-software/codex-cli": "0.3.4" }, "bin": { "codex-acp": "dist/adapter.js" } }, "sha512-rA0PQHlxX9P9GdUQUZPHkansHg+C//khRJzT3YYOY4Y24RxzM7v40jcQPGxiKe/HDudtJ9SyuuVIJx1aLHFcRw=="], - "@agentos-software/codex-cli": ["@agentos-software/codex-cli@0.3.4", "", {}, "sha512-SAw3EOTa90dJLgEVVoE7JJIxHwticzdD9cEM9v00gpxhxC9vdLkeBva6rgWIUB9+qD1JEYBvUCyNkIpD2kT5YQ=="], "@agentos-software/common": ["@agentos-software/common@0.2.14", "", { "dependencies": { "@agentos-software/coreutils": "0.3.4", "@agentos-software/diffutils": "0.3.4", "@agentos-software/findutils": "0.3.4", "@agentos-software/gawk": "0.3.4", "@agentos-software/grep": "0.3.4", "@agentos-software/gzip": "0.3.4", "@agentos-software/sed": "0.3.4", "@agentos-software/tar": "0.3.5" } }, "sha512-ve/ks2MZtXFN9JK+kcFnSMGI1tqoUy36sTKWYGqApcgg/3JJwqnMs4JIKL5OXjgCtDuTvTjRySbQvEKxGkeeSQ=="], @@ -1945,7 +1942,7 @@ "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], @@ -2929,7 +2926,7 @@ "parse-png": ["parse-png@2.1.0", "", { "dependencies": { "pngjs": "^3.3.0" } }, "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ=="], - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="], @@ -3785,8 +3782,6 @@ "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "cli-highlight/parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], - "cli-highlight/yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -3859,10 +3854,12 @@ "gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], - "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], - "googleapis-common/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "hast-util-from-html/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -4443,6 +4440,8 @@ "@expo/package-manager/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "@google/genai/google-auth-library/gaxios/node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "@react-native/dev-middleware/serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], diff --git a/deploy/zopu-runtime/.env.template b/deploy/zopu-runtime/.env.template index 1a0a780..9c3a510 100644 --- a/deploy/zopu-runtime/.env.template +++ b/deploy/zopu-runtime/.env.template @@ -25,14 +25,7 @@ CONVEX_INSTANCE_NAME=zopu-production CONVEX_INSTANCE_SECRET= # --------------------------------------------------------------------------- -# 2. Self-hosted Git / Gitea — REQUIRED for issue lifecycle -# The agent daemon clones repos and creates PRs through Gitea. -# --------------------------------------------------------------------------- -GITEA_URL=https://git.openputer.com -GITEA_TOKEN=replace-with-gitea-api-token - -# --------------------------------------------------------------------------- -# 3. Model gateway — REQUIRED +# 2. Model gateway — REQUIRED # All model calls route through this OpenAI-compatible endpoint. # --------------------------------------------------------------------------- AGENT_MODEL_PROVIDER=cheaptricks @@ -44,17 +37,17 @@ AGENT_MODEL_CONTEXT_WINDOW=262000 AGENT_MODEL_MAX_TOKENS=131072 # --------------------------------------------------------------------------- -# 4. AgentOS / Rivet Engine — REQUIRED for the execution runner -# The agent service and runner connect through the public engine endpoint. -# RIVET_WORKSPACE_TOKEN authenticates every workspace actor connection. -# RIVET_ENVOY_VERSION must change for each runner deployment. +# 3. AgentOS / Rivet Engine — REQUIRED for the execution runner +# The runner creates isolated worktrees from ZOPU_SOURCE_REPOSITORY and +# connects to AgentOS through the public engine endpoint. # --------------------------------------------------------------------------- RIVET_ENDPOINT=https://default:@rivet.example.com RIVET_PUBLIC_ENDPOINT=https://default@rivet.example.com RIVET_ENVOY_VERSION=1 RIVET_WORKSPACE_TOKEN=replace-with-a-long-random-workspace-token +ZOPU_SOURCE_REPOSITORY=/opt/zopu-source # --------------------------------------------------------------------------- -# 5. Zopu agent service (Flue) +# 4. Zopu agent service (Flue) # FLUE_DB_TOKEN authenticates the Flue persistence adapter. # zopu-agent.service pins the Flue Node server to port 3583. # --------------------------------------------------------------------------- @@ -62,7 +55,7 @@ FLUE_DB_TOKEN=replace-with-long-random-token AGENT_BACKEND_URL=https://zopu-agent.example.com # --------------------------------------------------------------------------- -# 6. Daemon identity +# 5. Daemon identity # --------------------------------------------------------------------------- DAEMON_ID=zopu-dedicated DAEMON_NAME=Zopu-Dedicated-Server diff --git a/deploy/zopu-runtime/runner/Dockerfile b/deploy/zopu-runtime/runner/Dockerfile index 7a6eb92..b16444b 100644 --- a/deploy/zopu-runtime/runner/Dockerfile +++ b/deploy/zopu-runtime/runner/Dockerfile @@ -5,7 +5,7 @@ FROM node:24-bookworm-slim COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun RUN apt-get update \ - && apt-get install -y --no-install-recommends g++ make python3 \ + && apt-get install -y --no-install-recommends ca-certificates g++ git make python3 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/deploy/zopu-runtime/runner/docker-compose.yml b/deploy/zopu-runtime/runner/docker-compose.yml index 17f7188..de48783 100644 --- a/deploy/zopu-runtime/runner/docker-compose.yml +++ b/deploy/zopu-runtime/runner/docker-compose.yml @@ -14,14 +14,14 @@ services: CONVEX_URL: ${CONVEX_URL} DAEMON_ID: zopu-agentos-runner FLUE_DB_TOKEN: ${FLUE_DB_TOKEN} - GITEA_TOKEN: ${GITEA_TOKEN} - GITEA_URL: ${GITEA_URL} RIVET_ENDPOINT: ${RIVET_ENDPOINT} RIVET_PUBLIC_ENDPOINT: ${RIVET_PUBLIC_ENDPOINT} RIVET_ENVOY_VERSION: ${RIVET_ENVOY_VERSION} RIVET_WORKSPACE_TOKEN: ${RIVET_WORKSPACE_TOKEN} RIVET_POOL: default + ZOPU_SOURCE_REPOSITORY: /opt/zopu-source volumes: + - ../..:/opt/zopu-source - zopu-agentos-workspaces:/var/lib/zopu/workspaces restart: unless-stopped diff --git a/packages/agents/package.json b/packages/agents/package.json index 25f9309..5f2fbc6 100644 --- a/packages/agents/package.json +++ b/packages/agents/package.json @@ -14,7 +14,6 @@ "run:work-planner": "bun --env-file=../../.env flue run work-planner" }, "dependencies": { - "@agentos-software/codex-cli": "0.3.4", "@agentos-software/git": "0.3.3", "@code/backend": "workspace:*", "@code/env": "workspace:*", diff --git a/packages/agents/src/runtime/agent-os.ts b/packages/agents/src/runtime/agent-os.ts index a2c4734..dd870ac 100644 --- a/packages/agents/src/runtime/agent-os.ts +++ b/packages/agents/src/runtime/agent-os.ts @@ -5,8 +5,9 @@ import { createClient } from "@rivet-dev/agentos/client"; import { Effect } from "effect"; import { - codexSessionEnv, - makeCodexAgentOsConfig, + makePiAgentOsConfig, + makePiHomeFiles, + piSessionEnv, } from "../../../primitives/src/agent-os"; import { decodeWorkAttemptExecutionInput, @@ -18,7 +19,7 @@ import type { } from "../../../primitives/src/execution-runtime"; import { HostRepositoryWorkspace } from "./host-repository"; -const codexConfig = makeCodexAgentOsConfig(); +const piConfig = makePiAgentOsConfig(); const workspace = agentOS({ onBeforeConnect: (_context, params) => { if (params.token !== process.env.RIVET_WORKSPACE_TOKEN) { @@ -28,8 +29,8 @@ const workspace = agentOS({ options: { actionTimeout: 10 * 60 * 1000, }, - permissions: codexConfig.permissions, - software: codexConfig.software, + permissions: piConfig.permissions, + software: piConfig.software, }); export const runtimeRegistry = setup({ use: { workspace } }); @@ -94,24 +95,61 @@ export const executeAgentOsAttempt = async ( }), ]; const hostRepository = new HostRepositoryWorkspace(); - const prepared = await hostRepository.prepare(input); + const prepared = await hostRepository.prepare({ + attemptId: input.attemptId, + piHomeFiles: makePiHomeFiles({ + api: env.AGENT_MODEL_API, + apiKeyEnvironmentVariable: "AGENT_MODEL_API_KEY", + baseUrl: env.AGENT_MODEL_BASE_URL, + contextWindow: env.AGENT_MODEL_CONTEXT_WINDOW, + maxTokens: env.AGENT_MODEL_MAX_TOKENS, + model: env.AGENT_MODEL_NAME, + provider: env.AGENT_MODEL_PROVIDER, + }), + }); events.push( event( 1, - prepared.cloned ? "repository.cloning" : "runtime.preparing", - prepared.cloned - ? "Project repository cloned on the execution host" - : "Host repository checkout reused" + "runtime.preparing", + prepared.created + ? "Isolated Zopu worktree created on the execution host" + : "Isolated Zopu worktree recreated" ) ); - await vm.mountFs({ - path: "/workspace/repository", - plugin: createHostDirBackend({ - hostPath: prepared.checkoutPath, + await Promise.all([ + vm.mountFs({ + path: "/workspace/repository", + plugin: createHostDirBackend({ + hostPath: prepared.checkoutPath, + readOnly: false, + }), readOnly: false, }), - readOnly: false, - }); + vm.mountFs({ + path: `${prepared.sourceRepositoryPath}/.git`, + plugin: createHostDirBackend({ + hostPath: `${prepared.sourceRepositoryPath}/.git`, + readOnly: false, + }), + readOnly: false, + }), + vm.mountFs({ + path: "/home/zopu", + plugin: createHostDirBackend({ + hostPath: prepared.piHomePath, + readOnly: false, + }), + readOnly: false, + }), + vm.mountFs({ + path: "/opt/zopu-tools", + plugin: createHostDirBackend({ + hostPath: prepared.toolsPath, + readOnly: true, + }), + readOnly: true, + }), + ]); const { baseRevision } = prepared; events.push( event(2, "repository.ready", "Repository checkout is ready", { @@ -119,22 +157,19 @@ export const executeAgentOsAttempt = async ( }) ); - const sessionId = `codex-${input.attemptId}`; + const sessionId = `pi-${input.attemptId}`; await vm.openSession({ + additionalDirectories: [`${prepared.sourceRepositoryPath}/.git`], additionalInstructions: - "Work only inside /workspace/repository. Do not reveal credentials. Make the requested change and run focused verification. Do not push or open a pull request.", - agent: "codex", + "Work only inside /workspace/repository. This is an isolated worktree of the Zopu product repository. Read AGENTS.md and the relevant product specifications before changing code. Never reveal credentials, modify the read-only base checkout, push, or open a pull request. Implement the requested change and run focused verification.", + agent: "pi", cwd: "/workspace/repository", - env: codexSessionEnv({ - apiKey: env.AGENT_MODEL_API_KEY, - baseUrl: env.AGENT_MODEL_BASE_URL, - model: env.AGENT_MODEL_NAME, - }), + env: piSessionEnv(env.AGENT_MODEL_API_KEY), permissionPolicy: "allow_all", sessionId, }); events.push( - event(3, "harness.started", "Codex implementation session started") + event(3, "harness.started", "Pi implementation session started") ); const promptResult = await vm.prompt({ content: [{ text: input.prompt, type: "text" }], @@ -145,14 +180,14 @@ export const executeAgentOsAttempt = async ( const reason = promptResult.stopReason === "cancelled" ? "Cancelled" : "HarnessFailed"; throw executionError( - `Codex stopped with ${promptResult.stopReason}`, + `Pi stopped with ${promptResult.stopReason}`, reason, promptResult.stopReason === "max_tokens" || promptResult.stopReason === "max_turn_requests" ); } events.push( - event(4, "harness.progress", "Codex implementation turn completed", { + event(4, "harness.progress", "Pi implementation turn completed", { stopReason: promptResult.stopReason, }) ); @@ -184,8 +219,8 @@ export const executeAgentOsAttempt = async ( events, summary: changedFiles.length > 0 - ? `Codex changed ${changedFiles.length} file(s)` - : "Codex completed without repository changes", + ? `Pi changed ${changedFiles.length} file(s)` + : "Pi completed without repository changes", }; } catch (error) { throw classifyRuntimeFailure(error); @@ -205,5 +240,5 @@ export const cancelAgentOsAttempt = async ( .getOrCreate([workspaceKey], { params: { token: env.RIVET_WORKSPACE_TOKEN }, }) - .cancelPrompt({ sessionId: `codex-${attemptId}` }); + .cancelPrompt({ sessionId: `pi-${attemptId}` }); }; diff --git a/packages/agents/src/runtime/host-repository.ts b/packages/agents/src/runtime/host-repository.ts index 41a5904..0804090 100644 --- a/packages/agents/src/runtime/host-repository.ts +++ b/packages/agents/src/runtime/host-repository.ts @@ -1,21 +1,30 @@ +import { execFileSync, spawn } from "node:child_process"; import { createHash } from "node:crypto"; -import { mkdir } from "node:fs/promises"; +import { once } from "node:events"; +import { + access, + chmod, + copyFile, + mkdir, + rm, + writeFile, +} from "node:fs/promises"; import path from "node:path"; -import type { RepositoryExecutionAuth } from "../../../primitives/src/execution-runtime"; +import type { PiHomeFiles } from "../../../primitives/src/agent-os"; interface PrepareRepositoryInput { - auth: Pick; - baseBranch: string; - repositoryUrl: string; - runId: string; - workspaceKey: string; + attemptId: string; + piHomeFiles: PiHomeFiles; } interface PreparedRepository { baseRevision: string; checkoutPath: string; - cloned: boolean; + created: boolean; + piHomePath: string; + sourceRepositoryPath: string; + toolsPath: string; } interface CollectRepositoryInput { @@ -30,30 +39,19 @@ interface CollectedRepository { diff: string; } -interface GitResult { +interface ProcessResult { exitCode: number; stderr: string; stdout: string; } -const requireSuccess = (result: GitResult, operation: string): string => { +const requireSuccess = (result: ProcessResult, 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 => ({ - 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 = " -> "; @@ -63,99 +61,138 @@ const changedFilePath = (line: string): string => { : filePath.slice(renameIndex + renameSeparator.length); }; -const runGit = async ( - cwd: string, +const runProcess = async ( + command: string, args: readonly string[], + cwd: string, env: Record = {} -): Promise => { +): Promise => { const environment = Object.fromEntries( Object.entries(process.env).filter( (entry): entry is [string, string] => entry[1] !== undefined ) ); - const child = Bun.spawn(["git", ...args], { + const child = spawn(command, 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 }; + const stderr: Uint8Array[] = []; + const stdout: Uint8Array[] = []; + child.stderr.on("data", (chunk: Uint8Array) => stderr.push(chunk)); + child.stdout.on("data", (chunk: Uint8Array) => stdout.push(chunk)); + const [exitCode] = await once(child, "close"); + return { + exitCode: typeof exitCode === "number" ? exitCode : 1, + stderr: Buffer.concat(stderr).toString(), + stdout: Buffer.concat(stdout).toString(), + }; +}; + +const runGit = (cwd: string, args: readonly string[]) => + runProcess("git", args, cwd); + +const pathExists = async (target: string): Promise => { + try { + await access(target); + return true; + } catch { + return false; + } }; export class HostRepositoryWorkspace { readonly #root: string; + readonly #sourceRepositoryPath: string; + readonly #installDependencies: boolean; constructor( - root = process.env.AGENT_WORKSPACE_ROOT ?? "/var/lib/zopu/workspaces" + root = process.env.AGENT_WORKSPACE_ROOT ?? "/var/lib/zopu/workspaces", + sourceRepositoryPath = process.env.ZOPU_SOURCE_REPOSITORY ?? + "/opt/zopu-source", + installDependencies = true ) { this.#root = root; + this.#sourceRepositoryPath = sourceRepositoryPath; + this.#installDependencies = installDependencies; } async prepare(input: PrepareRepositoryInput): Promise { - 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" + if (!(await pathExists(path.join(this.#sourceRepositoryPath, ".git")))) { + throw new Error( + `Fixed Zopu source repository is unavailable at ${this.#sourceRepositoryPath}` ); } + const identity = createHash("sha256") + .update(input.attemptId) + .digest("hex") + .slice(0, 24); + const workspacePath = path.join(this.#root, identity); + const checkoutPath = path.join(workspacePath, "repository"); + const toolsPath = path.join(workspacePath, "tools"); + const piHomePath = path.join(workspacePath, "home"); + const branch = `zopu/attempt-${identity}`; + const created = !(await pathExists(path.join(checkoutPath, ".git"))); + + await mkdir(workspacePath, { recursive: true }); + if (!created) { + requireSuccess( + await runGit(this.#sourceRepositoryPath, [ + "worktree", + "remove", + "--force", + checkoutPath, + ]), + "Existing worktree removal" + ); + } + await rm(checkoutPath, { force: true, recursive: true }); requireSuccess( - await runGit( - checkoutPath, - ["fetch", "origin", input.baseBranch], - authEnv - ), - "Repository fetch" - ); - requireSuccess( - await runGit(checkoutPath, [ - "checkout", + await runGit(this.#sourceRepositoryPath, [ + "worktree", + "add", "-B", - `zopu/${input.runId}`, - `origin/${input.baseBranch}`, + branch, + checkoutPath, + "HEAD", ]), - "Repository checkout" + "Zopu worktree creation" ); - requireSuccess( - await runGit(checkoutPath, ["reset", "--hard", "HEAD"]), - "Repository reset" + + const sourceEnvPath = path.join(this.#sourceRepositoryPath, ".env"); + if (await pathExists(sourceEnvPath)) { + await copyFile(sourceEnvPath, path.join(checkoutPath, ".env")); + } + if (this.#installDependencies) { + requireSuccess( + await runProcess( + "bun", + ["install", "--frozen-lockfile"], + checkoutPath, + { CI: "1" } + ), + "Workspace dependency installation" + ); + } + + const toolsBinPath = path.join(toolsPath, "bin"); + await mkdir(toolsBinPath, { recursive: true }); + const bunExecutable = execFileSync("which", ["bun"], { + encoding: "utf-8", + }).trim(); + const bunPath = path.join(toolsBinPath, "bun"); + await copyFile(bunExecutable, bunPath); + await chmod(bunPath, 0o755); + + const piAgentPath = path.join(piHomePath, ".pi", "agent"); + await mkdir(piAgentPath, { recursive: true }); + await writeFile( + path.join(piAgentPath, "models.json"), + input.piHomeFiles.models ); - requireSuccess( - await runGit(checkoutPath, ["clean", "-fdx"]), - "Repository clean" + await writeFile( + path.join(piAgentPath, "settings.json"), + input.piHomeFiles.settings ); return { @@ -164,7 +201,10 @@ export class HostRepositoryWorkspace { "Base revision lookup" ), checkoutPath, - cloned, + created, + piHomePath, + sourceRepositoryPath: this.#sourceRepositoryPath, + toolsPath, }; } @@ -202,8 +242,8 @@ export class HostRepositoryWorkspace { "Candidate tree creation" ); candidateRevision = requireSuccess( - await runGit( - input.checkoutPath, + await runProcess( + "git", [ "commit-tree", tree, @@ -212,6 +252,7 @@ export class HostRepositoryWorkspace { "-m", `Zopu candidate for ${input.attemptId}`, ], + input.checkoutPath, { GIT_AUTHOR_EMAIL: "agent@zopu.dev", GIT_AUTHOR_NAME: "Zopu Agent", diff --git a/packages/backend/convex/workExecutionAgent.ts b/packages/backend/convex/workExecutionAgent.ts index ffd0774..e1af4cf 100644 --- a/packages/backend/convex/workExecutionAgent.ts +++ b/packages/backend/convex/workExecutionAgent.ts @@ -81,7 +81,7 @@ export const cancelAttempt = internalAction({ handler: async (_ctx, args) => { // The workspace key remains the URL path segment (workspace identity), // while the body carries the attemptId so the runtime can target the - // codex session `codex-${attemptId}` for cancellation. + // matching Pi ACP session for cancellation. await fetch( `${backendUrl()}/internal/work-attempts/${encodeURIComponent(args.workspaceKey)}/cancel`, { diff --git a/packages/backend/convex/workExecutionWorkflow.ts b/packages/backend/convex/workExecutionWorkflow.ts index 9c11810..78c54a7 100644 --- a/packages/backend/convex/workExecutionWorkflow.ts +++ b/packages/backend/convex/workExecutionWorkflow.ts @@ -430,7 +430,7 @@ export const completeAttempt = internalMutation({ kind: "diff", metadataJson: JSON.stringify({ changedFiles: args.result.changedFiles }), organizationId: work.organizationId, - producer: "agentos-codex", + producer: "agentos-pi", projectId: work.projectId, provenanceJson: JSON.stringify({ baseRevision: args.result.baseRevision, diff --git a/packages/env/src/agent.ts b/packages/env/src/agent.ts index 434b1f8..e93d96b 100644 --- a/packages/env/src/agent.ts +++ b/packages/env/src/agent.ts @@ -17,6 +17,7 @@ const agentEnvSchema = z.object({ RIVET_ENDPOINT: z.url(), RIVET_PUBLIC_ENDPOINT: z.url().optional(), RIVET_WORKSPACE_TOKEN: z.string().min(32), + ZOPU_SOURCE_REPOSITORY: z.string().min(1).default("/opt/zopu-source"), }); export type AgentEnv = z.infer; diff --git a/packages/primitives/package.json b/packages/primitives/package.json index 936bfb7..92b3a07 100644 --- a/packages/primitives/package.json +++ b/packages/primitives/package.json @@ -30,7 +30,7 @@ "test:watch": "vitest" }, "dependencies": { - "@agentos-software/codex": "0.0.0-agent-acp-codex-session-benchmark.fd745e7", + "@agentos-software/pi": "0.2.7", "@agentos-software/git": "0.3.3", "@rivet-dev/agentos": "0.2.14", "effect": "catalog:" diff --git a/packages/primitives/src/agent-os.test.ts b/packages/primitives/src/agent-os.test.ts deleted file mode 100644 index b87abf1..0000000 --- a/packages/primitives/src/agent-os.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { codexSessionEnv, makeCodexAgentOsConfig } from "./agent-os"; - -describe("Codex agent-os config", () => { - it("always includes the codex + git software bundle", () => { - const config = makeCodexAgentOsConfig(); - expect(config.software).toHaveLength(2); - expect(config.software?.[0]).toBeTypeOf("object"); - }); - - it("allows repository execution capabilities", () => { - const config = makeCodexAgentOsConfig(); - expect(config.permissions?.network).toBe("allow"); - expect(config.permissions?.fs).toBe("allow"); - expect(config.permissions?.childProcess).toBe("allow"); - expect(config.permissions?.env).toBe("allow"); - expect(config.permissions?.process).toBe("allow"); - }); - - it("builds model-provider session env", () => { - const env = codexSessionEnv( - { - apiKey: "sk-test", - baseUrl: "https://ai.example.com/v1", - model: "glm-5.2", - }, - { CODEX_MODEL: "glm-5.2" } - ); - expect(env.OPENAI_API_KEY).toBe("sk-test"); - expect(env.OPENAI_BASE_URL).toBe("https://ai.example.com/v1"); - expect(env.CODEX_MODEL).toBe("glm-5.2"); - }); -}); diff --git a/packages/primitives/src/agent-os.ts b/packages/primitives/src/agent-os.ts index 439834a..7ca5fc8 100644 --- a/packages/primitives/src/agent-os.ts +++ b/packages/primitives/src/agent-os.ts @@ -1,5 +1,5 @@ /* eslint-disable max-classes-per-file -- the AgentOS service and its tagged error form one adapter contract. */ -import codex from "@agentos-software/codex"; +import pi from "@agentos-software/pi"; import { agentOS as createAgentOsActor } from "@rivet-dev/agentos"; import type { AgentOSConfigInput } from "@rivet-dev/agentos"; import { Context, Effect, Layer, Schema } from "effect"; @@ -73,55 +73,86 @@ export const createAgentOsActorEffect = Effect.fn("createAgentOsActorEffect")( ); // --------------------------------------------------------------------------- -// Codex harness configuration +// Pi harness configuration // -// The canonical execution registry runs exactly one AgentOS actor for the -// puter/zopu-code repo, booted with the Codex CLI harness + git. The model -// provider is injected as VM env (OpenAI-compatible base URL + key) so Codex -// authenticates against the configured gateway without a second credentials -// path. This is a pure config builder; the RivetKit registry/server that hosts -// the actor lives in the agents package. +// Slice 5 intentionally runs one harness against the server's fixed Zopu +// checkout. Pi speaks ACP natively and reads its model gateway from a mounted +// HOME, avoiding a second executable adapter or credentials path. // --------------------------------------------------------------------------- -/** Software bundle for a Codex-capable VM: the Codex harness plus git. */ -export const codexSoftware = [...codex, getExecutableGitSoftware()] as const; +/** Software bundle for a Pi-capable VM: the Pi ACP harness plus git. */ +export const piSoftware = [pi, 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"`). */ -export interface CodexModelEnv { - readonly apiKey: string; +export interface PiModelConfig { + readonly api: "openai-completions"; + readonly apiKeyEnvironmentVariable: string; readonly baseUrl: string; + readonly contextWindow: number; + readonly maxTokens: number; readonly model: string; + readonly provider: string; } -/** Builds the VM env record Codex reads to authenticate against the model gateway. */ -export const codexSessionEnv = ( - model: CodexModelEnv, +export interface PiHomeFiles { + readonly models: string; + readonly settings: string; +} + +/** Builds the two files Pi reads from ~/.pi/agent. */ +export const makePiHomeFiles = (model: PiModelConfig): PiHomeFiles => ({ + models: JSON.stringify( + { + providers: { + [model.provider]: { + api: model.api, + apiKey: model.apiKeyEnvironmentVariable, + authHeader: true, + baseUrl: model.baseUrl, + models: [ + { + contextWindow: model.contextWindow, + id: model.model, + maxTokens: model.maxTokens, + name: model.model, + }, + ], + }, + }, + }, + null, + 2 + ), + settings: JSON.stringify( + { + defaultModel: model.model, + defaultProvider: model.provider, + quietStartup: true, + }, + null, + 2 + ), +}); + +/** Environment supplied to the Pi ACP session. */ +export const piSessionEnv = ( + apiKey: string, extra: Readonly> = {} ): Record => ({ - CODEX_HOME: "/tmp/codex-home", - CODEX_MODEL: model.model, - HOME: "/tmp", - OPENAI_API_KEY: model.apiKey, - OPENAI_BASE_URL: model.baseUrl, + AGENT_MODEL_API_KEY: apiKey, + HOME: "/home/zopu", + PATH: "/opt/zopu-tools/bin:/usr/local/bin:/usr/bin:/bin", ...extra, }); -export interface CodexAgentOsConfigInput { - /** Extra software merged after the Codex+git bundle. */ +export interface PiAgentOsConfigInput { + /** Extra software merged after the Pi+git bundle. */ readonly software?: NonNullable["software"]>; /** VM permission overrides merged over the execution policy. */ readonly permissions?: AgentOSConfigInput["permissions"]; } -/** - * are always present; software the caller passes is appended, never replacing - * the harness or git. Filesystem, process execution, environment variables, and - * outbound network access are enabled for repository and model-provider work. - * Model-provider env is supplied per session via `codexSessionEnv`. - */ -export const makeCodexAgentOsConfig = ( - input: CodexAgentOsConfigInput = {} +export const makePiAgentOsConfig = ( + input: PiAgentOsConfigInput = {} ): AgentOSConfigInput => ({ permissions: { childProcess: "allow", @@ -131,5 +162,5 @@ export const makeCodexAgentOsConfig = ( process: "allow", ...input.permissions, }, - software: [...codexSoftware, ...(input.software ?? [])], + software: [...piSoftware, ...(input.software ?? [])], });