Skip to content
Closed
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
4 changes: 0 additions & 4 deletions apps/code/src/main/di/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,7 @@ import type {
} from "@posthog/workspace-server/services/mcp-relay/identifiers";
import type {
PI_RPC_CLIENT_FACTORY,
PI_RUNTIME_FACTORY,
PiRpcClientFactory,
PiRuntimeFactory,
} from "@posthog/workspace-server/services/pi-session/identifiers";
import type { PosthogPluginService } from "@posthog/workspace-server/services/posthog-plugin/posthog-plugin";
import type { ProcessTrackingService } from "@posthog/workspace-server/services/process-tracking/process-tracking";
Expand Down Expand Up @@ -358,8 +356,6 @@ export interface MainBindings {
[AGENT_LOGGER]: RootLogger;
[PI_RPC_CLIENT_FACTORY]: PiRpcClientFactory;

[PI_RUNTIME_FACTORY]: PiRuntimeFactory;

// Logger
[ROOT_LOGGER]: RootLogger;

Expand Down
3 changes: 0 additions & 3 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ import { onboardingImportModule } from "@posthog/workspace-server/services/onboa
import { osModule } from "@posthog/workspace-server/services/os/os.module";
import {
PI_RPC_CLIENT_FACTORY,
PI_RUNTIME_FACTORY,
PI_SESSION_SERVICE,
} from "@posthog/workspace-server/services/pi-session/identifiers";
import type { PiSessionService } from "@posthog/workspace-server/services/pi-session/pi-session";
Expand Down Expand Up @@ -231,7 +230,6 @@ import { workspaceMetadataModule } from "@posthog/workspace-server/services/work
import ExternalAppsStoreImpl from "electron-store";
import type { FileWatcherBridge } from "../index";
import { DesktopPiRpcClientFactory } from "../platform-adapters/desktop-pi-rpc-client-factory";
import { DesktopPiRuntimeFactory } from "../platform-adapters/desktop-pi-runtime-factory";
import { ElectronAppLifecycle } from "../platform-adapters/electron-app-lifecycle";
import { ElectronAppMeta } from "../platform-adapters/electron-app-meta";
import { ElectronAppMetrics } from "../platform-adapters/electron-app-metrics";
Expand Down Expand Up @@ -379,7 +377,6 @@ container
.bind(MAIN_DEFAULT_ADDITIONAL_DIRECTORY_REPOSITORY)
.toService(DEFAULT_ADDITIONAL_DIRECTORY_REPOSITORY);
container.load(agentModule);
container.bind(PI_RUNTIME_FACTORY).to(DesktopPiRuntimeFactory);
container.load(piSessionModule);
container.bind(AGENT_SLEEP_COORDINATOR).toService(MAIN_SLEEP_SERVICE);
container.bind(AGENT_MCP_APPS).toService(MCP_APPS_SERVICE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe("DesktopPiRpcClientFactory", () => {
);
expect(createPiRpcClient).toHaveBeenCalledWith({
cwd: "/workspace",
capabilities: { environment: "local" },
providerOptions: {
region: "eu",
baseUrl: "http://127.0.0.1:1234",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class DesktopPiRpcClientFactory implements PiRpcClientFactory {

return createPiRpcClient({
...input,
capabilities: { environment: "local" },
providerOptions: {
region: credentials.region,
baseUrl,
Expand Down

This file was deleted.

24 changes: 0 additions & 24 deletions apps/code/src/main/platform-adapters/desktop-pi-runtime-factory.ts

This file was deleted.

41 changes: 28 additions & 13 deletions apps/code/vite-main-plugins.mts
Original file line number Diff line number Diff line change
Expand Up @@ -167,21 +167,36 @@ export function copyPiRpcHost(): Plugin {
return {
name: "copy-pi-rpc-host",
writeBundle() {
const candidates = [
join(__dirname, "node_modules/@posthog/agent/dist/pi/rpc-host.js"),
join(
__dirname,
"../../node_modules/@posthog/agent/dist/pi/rpc-host.js",
),
join(__dirname, "../../packages/agent/dist/pi/rpc-host.js"),
const assets = [
{
name: "Pi RPC host",
source: "pi/rpc-host.js",
destination: "rpc-host.js",
},
{
name: "Pi local-tools MCP server",
source: "adapters/local-tools/mcp-server.js",
destination: "mcp-server.js",
},
];
const source = candidates.find((candidate) => existsSync(candidate));
if (!source) {
throw new Error(
`[copy-pi-rpc-host] Unable to find Pi RPC host, required at runtime by createPiRpcClient. Build @posthog/agent first. Checked:\n ${candidates.join("\n ")}`,
);

for (const asset of assets) {
const candidates = [
join(__dirname, `node_modules/@posthog/agent/dist/${asset.source}`),
join(
__dirname,
`../../node_modules/@posthog/agent/dist/${asset.source}`,
),
join(__dirname, `../../packages/agent/dist/${asset.source}`),
];
const source = candidates.find((candidate) => existsSync(candidate));
if (!source) {
throw new Error(
`[copy-pi-rpc-host] Unable to find ${asset.name}. Build @posthog/agent first. Checked:\n ${candidates.join("\n ")}`,
);
}
copyFileSync(source, join(__dirname, ".vite/build", asset.destination));
}
copyFileSync(source, join(__dirname, ".vite/build/rpc-host.js"));
},
};
}
Expand Down
2 changes: 2 additions & 0 deletions docs/cloud-task-artifacts.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ Cloud agents run with `/tmp/workspace` as the sandbox workspace. A repository ta

Files left in the sandbox filesystem are temporary and cannot be downloaded after the sandbox is released. Agents should call the `upload_artifact` tool for every non-code deliverable they create. The tool accepts files inside the session working directory, uploads them directly to task object storage, and registers them as `output` artifacts on the task run. Registered artifacts are available through the existing task artifact download endpoint.

On success the tool returns a presigned download URL for the uploaded file (minted by the finalize-upload endpoint), so an agent can link to the artifact directly in its final response. The URL is time-limited and not persisted on the run manifest; re-fetch the artifact through the download endpoint for a durable link.

Repository changes should continue to be delivered through git rather than duplicated as task artifacts. A single uploaded artifact is limited to 30 MB.
6 changes: 3 additions & 3 deletions packages/agent/build/verify-local-tools-mcp-server.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Post-build smoke check: boot the bundled local-tools MCP server exactly the
// way Codex spawns it in a cloud sandbox (a bare `node dist/...js` process) and
// way an agent runtime spawns it in a cloud sandbox (a bare `node dist/...js` process) and
// assert it answers `tools/list`. Catches bundling regressions — e.g. inlined
// CJS deps whose dynamic `require()` throws in ESM output — that unit tests
// running from src can never see, and that otherwise fail silently in cloud
Expand All @@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";

const script = resolve(
fileURLToPath(new URL(".", import.meta.url)),
"../dist/adapters/codex-app-server/local-tools-mcp-server.js",
"../dist/adapters/local-tools/mcp-server.js",
);

const ctx = Buffer.from(
Expand All @@ -20,7 +20,7 @@ const ctx = Buffer.from(
const child = spawn(process.execPath, [script], {
env: {
...process.env,
// Mirror the production Codex spawn: if this ever runs from an
// Mirror the production child spawn: if this ever runs from an
// Electron-hosted process, execPath is the app binary, not node.
ELECTRON_RUN_AS_NODE: "1",
POSTHOG_LOCAL_TOOLS_CTX: ctx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ import {
estimateTokens,
} from "../claude/context-breakdown";
import { isLocalSkillCommandChunk } from "../local-skill";
import {
buildLocalToolsServer,
type LocalToolsMeta,
} from "../local-tools/mcp-server-config";
import { resolveSpokenNarration } from "../session-meta";
import {
AppServerClient,
Expand All @@ -67,7 +71,6 @@ import {
buildUsageBreakdownParams,
} from "./ext-notifications";
import { type CodexUserInput, toCodexInput } from "./input";
import { buildLocalToolsServer, type LocalToolsMeta } from "./local-tools-mcp";
import {
type AppServerItem,
changePaths,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { LOCAL_TOOLS_MCP_NAME } from "../local-tools";
import { buildLocalToolsServer } from "./local-tools-mcp";
import { buildLocalToolsServer } from "./mcp-server-config";

// The dist asset isn't on the walk-up path in unit tests, so make existsSync
// succeed; nothing spawns the script — we only inspect the path.
Expand Down Expand Up @@ -55,7 +55,7 @@ describe("buildLocalToolsServer", () => {
expect(server?.name).toBe(LOCAL_TOOLS_MCP_NAME);
expect(server?.command).toBe(process.execPath);
expect(server?.args).toHaveLength(1);
expect(server?.args[0]).toMatch(/local-tools-mcp-server\.js$/);
expect(server?.args[0]).toMatch(/mcp-server\.js$/);

const envNames = server?.env.map((e) => e.name) ?? [];
expect(envNames).toContain("POSTHOG_LOCAL_TOOLS_CTX");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
/**
* Builds the stdio local-tools MCP server config to inject into a Codex
* app-server thread's `config.mcp_servers`.
* Returns the ACP `McpServerStdio` shape so the existing translation layer stays
* the single owner of the ACP→Codex map.
* Builds the stdio local-tools MCP server config for agent runtimes.
* Returns the ACP `McpServerStdio` shape used by the Codex adapter and Pi.
*/

import type { McpServerStdio } from "@agentclientprotocol/sdk";
import { ghTokenEnv } from "@posthog/git/signed-commit";
import { resolveGithubToken } from "../../utils/github-token";
import { resolveBundledMcpScript } from "../../utils/resolve-bundled-script";
import { resolveTaskId } from "../session-meta";
import {
enabledLocalTools,
LOCAL_TOOLS_MCP_NAME,
type LocalToolCtx,
type LocalToolGateMeta,
} from "../local-tools";
import { resolveTaskId } from "../session-meta";
} from "./index";

/**
* Gate inputs the local-tools server needs beyond `LocalToolGateMeta`: the task id
Expand All @@ -34,7 +32,7 @@ function toMcpServerStdio(
enabledNames: string[],
): McpServerStdio {
const scriptPath = resolveBundledMcpScript(
"adapters/codex-app-server/local-tools-mcp-server.js",
"adapters/local-tools/mcp-server.js",
);
const ctxBase64 = Buffer.from(JSON.stringify(ctx)).toString("base64");
const env = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { afterEach, describe, expect, it } from "vitest";

const directories: string[] = [];

async function temporaryDirectory(): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), "local-tools-mcp-server-"));
directories.push(directory);
return directory;
}

afterEach(async () => {
await Promise.all(
directories
.splice(0)
.map((directory) => rm(directory, { force: true, recursive: true })),
);
});

describe("local-tools MCP server", () => {
it("starts the bundled server and executes an enabled tool", async () => {
const cwd = await temporaryDirectory();
const client = new Client({ name: "local-tools-test", version: "1.0.0" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [
join(process.cwd(), "../../node_modules/tsx/dist/cli.mjs"),
join(process.cwd(), "src/adapters/local-tools/mcp-server.ts"),
],
cwd,
env: {
PATH: process.env.PATH ?? "",
POSTHOG_LOCAL_TOOLS_CTX: Buffer.from(
JSON.stringify({ cwd, taskId: "task-1", taskRunId: "run-1" }),
).toString("base64"),
POSTHOG_LOCAL_TOOLS_ENABLED: "speak",
},
});

try {
await client.connect(transport);
const tools = await client.listTools();
const result = await client.callTool({
name: "speak",
arguments: { text: "Completed the task", kind: "done" },
});

expect(tools.tools.map((tool) => tool.name)).toEqual(["speak"]);
expect(result.content).toEqual([{ type: "text", text: "ok" }]);
} finally {
await client.close();
}
});
});
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
/**
* Standalone stdio MCP server exposing the general local tools to the Codex
* app-server adapter, which spawns it as an MCP server process. Reads its context
* (cwd, taskId, token) from POSTHOG_LOCAL_TOOLS_CTX and the set of tools to
* register from POSTHOG_LOCAL_TOOLS_ENABLED (both set by the parent, which has
* already evaluated each tool's gate) — then registers those registry tools,
* the same ones the Claude adapter exposes in-process.
* Standalone stdio MCP server exposing the general local tools to agent runtimes.
* It reads its context from POSTHOG_LOCAL_TOOLS_CTX and its enabled tool set from
* POSTHOG_LOCAL_TOOLS_ENABLED, then registers the corresponding registry tools.
*
* Usage:
* POSTHOG_LOCAL_TOOLS_CTX=<base64> \
* POSTHOG_LOCAL_TOOLS_ENABLED=git_signed_commit \
* node local-tools-mcp-server.js
* node mcp-server.js
*/

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readGithubTokenFromEnv } from "@posthog/git/signed-commit";
import {
LOCAL_TOOLS,
LOCAL_TOOLS_MCP_NAME,
type LocalToolCtx,
} from "../local-tools";
import { LOCAL_TOOLS, LOCAL_TOOLS_MCP_NAME, type LocalToolCtx } from "./index";

function die(message: string): never {
process.stderr.write(`[local-tools-mcp-server] ${message}\n`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe("uploadArtifactTool", () => {
size: 7,
storage_path: "tasks/artifacts/report.csv",
uploaded_at: "2026-01-01T00:00:00Z",
url: "https://storage.example/download/report.csv",
},
]);
});
Expand Down Expand Up @@ -83,6 +84,9 @@ describe("uploadArtifactTool", () => {
}),
],
);
expect(result.content[0]?.text).toContain(
"https://storage.example/download/report.csv",
);
});

it("rejects files outside the session workspace", async () => {
Expand Down
Loading
Loading