Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ jobs:
- name: Build enricher
run: pnpm --filter @posthog/enricher build

- name: Build harness
run: pnpm --filter @posthog/harness build

- name: Build agent
run: pnpm --filter agent build

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/code-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ jobs:
pnpm --filter @posthog/shared run build
pnpm --filter @posthog/git run build
pnpm --filter @posthog/enricher run build
pnpm --filter @posthog/harness run build
pnpm --filter @posthog/agent run build

# build/Assets.car is gitignored; regenerate it so the packaged app ships
Expand Down Expand Up @@ -205,6 +206,7 @@ jobs:
pnpm --filter @posthog/shared run build
pnpm --filter @posthog/git run build
pnpm --filter @posthog/enricher run build
pnpm --filter @posthog/harness run build
pnpm --filter @posthog/agent run build

- name: Build app
Expand Down Expand Up @@ -275,6 +277,7 @@ jobs:
pnpm --filter @posthog/shared run build
pnpm --filter @posthog/git run build
pnpm --filter @posthog/enricher run build
pnpm --filter @posthog/harness run build
pnpm --filter @posthog/agent run build

- name: Build app
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/code-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ jobs:
- name: Build enricher package
run: pnpm --filter @posthog/enricher run build

- name: Build harness package
run: pnpm --filter @posthog/harness run build

- name: Build agent package
run: pnpm --filter @posthog/agent run build

Expand Down Expand Up @@ -331,6 +334,9 @@ jobs:
- name: Build enricher package
run: pnpm --filter @posthog/enricher run build

- name: Build harness package
run: pnpm --filter @posthog/harness run build

- name: Build agent package
run: pnpm --filter @posthog/agent run build

Expand Down Expand Up @@ -457,6 +463,9 @@ jobs:
- name: Build enricher package
run: pnpm --filter @posthog/enricher run build

- name: Build harness package
run: pnpm --filter @posthog/harness run build

- name: Build agent package
run: pnpm --filter @posthog/agent run build

Expand Down
16 changes: 10 additions & 6 deletions .github/workflows/code-storybook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,20 @@ jobs:
pid2=$!
wait_all "$pid1" "$pid2"

# @posthog/agent imports @posthog/git's dist, so git must finish
# before the last group starts.
pnpm --filter @posthog/git build

pnpm --filter @posthog/enricher build &
# @posthog/agent imports the dist of @posthog/git and
# @posthog/harness, so both must finish before the last group starts.
pnpm --filter @posthog/git build &
pid3=$!
pnpm --filter @posthog/agent build &
pnpm --filter @posthog/harness build &
pid4=$!
wait_all "$pid3" "$pid4"

pnpm --filter @posthog/enricher build &
pid5=$!
pnpm --filter @posthog/agent build &
pid6=$!
wait_all "$pid5" "$pid6"

- name: Build Storybook
working-directory: apps/code
run: pnpm build-storybook
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/code-update-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ jobs:
pnpm --filter @posthog/shared run build
pnpm --filter @posthog/git run build
pnpm --filter @posthog/enricher run build
pnpm --filter @posthog/harness run build
pnpm --filter @posthog/agent run build

- name: Build old + new update pair
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr-build-installer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ jobs:
pnpm --filter @posthog/shared run build
pnpm --filter @posthog/git run build
pnpm --filter @posthog/enricher run build
pnpm --filter @posthog/harness run build
pnpm --filter @posthog/agent run build

# build/Assets.car is gitignored; regenerate it so the packaged app ships
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,9 @@ jobs:
pnpm --filter @posthog/shared build
pnpm --filter @posthog/git build
pnpm --filter @posthog/enricher build
pnpm --filter agent build &
wait
pnpm --filter @posthog/harness build
pnpm --filter agent build

- name: Package Electron app
run: pnpm --filter code run package
Expand Down
6 changes: 6 additions & 0 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@
"types": "./dist/handoff-checkpoint.d.ts",
"import": "./dist/handoff-checkpoint.js"
},
"./pi": {
"types": "./dist/pi/rpc-client.d.ts",
"import": "./dist/pi/rpc-client.js"
},
"./server": {
"types": "./dist/server/agent-server.d.ts",
"import": "./dist/server/agent-server.js"
Expand Down Expand Up @@ -136,13 +140,15 @@
"@agentclientprotocol/sdk": "1.1.0",
"@anthropic-ai/claude-agent-sdk": "0.3.197",
"@anthropic-ai/sdk": "0.109.0",
"@earendil-works/pi-coding-agent": "catalog:",
"@hono/node-server": "^1.19.9",
"@openai/codex": "0.140.0",
"@opentelemetry/api-logs": "^0.208.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
"@opentelemetry/resources": "^2.0.0",
"@opentelemetry/sdk-logs": "^0.208.0",
"@opentelemetry/semantic-conventions": "^1.28.0",
"@posthog/harness": "workspace:*",
"@types/jsonwebtoken": "^9.0.10",
"commander": "^14.0.2",
"fflate": "^0.8.2",
Expand Down
26 changes: 26 additions & 0 deletions packages/agent/src/pi/rpc-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { RpcClient } from "@earendil-works/pi-coding-agent";
import { describe, expect, it } from "vitest";
import { createPiRpcClient } from "./rpc-client";

describe("createPiRpcClient", () => {
it("does not put provider credentials in the child environment", () => {
const client = createPiRpcClient({
cwd: "/workspace",
model: "claude-opus-4-8",
providerOptions: { apiKey: "token", region: "us" },
});

expect(client).toBeInstanceOf(RpcClient);
expect(client).toMatchObject({
options: {
cwd: "/workspace",
model: "claude-opus-4-8",
provider: "posthog",
},
});
expect(
(client as unknown as { options: { env?: Record<string, string> } })
.options.env,
).toBeUndefined();
});
});
152 changes: 152 additions & 0 deletions packages/agent/src/pi/rpc-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { type ChildProcess, spawn } from "node:child_process";
import type { Writable } from "node:stream";
import { StringDecoder } from "node:string_decoder";
import { fileURLToPath } from "node:url";
import {
RpcClient,
type RpcClientOptions,
} from "@earendil-works/pi-coding-agent";
import type { PosthogProviderOptions } from "@posthog/harness/extensions/posthog-provider/provider";
import { safePiEnvironment } from "./rpc-environment";

export type PiRpcClient = RpcClient;

interface RpcClientInternals {
process?: ChildProcess;
stopReadingStdout?: () => void;
stderr: string;
exitError: Error | null;
handleLine(line: string): void;
createProcessExitError(
code: number | null,
signal: NodeJS.Signals | null,
): Error;
rejectPendingRequests(error: Error): void;
}

function attachJsonlReader(
stream: NodeJS.ReadableStream,
onLine: (line: string) => void,
): () => void {
const decoder = new StringDecoder("utf8");
let buffer = "";
const onData = (chunk: Buffer | string) => {
buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
let newlineIndex = buffer.indexOf("\n");
while (newlineIndex !== -1) {
const line = buffer.slice(0, newlineIndex);
onLine(line.endsWith("\r") ? line.slice(0, -1) : line);
buffer = buffer.slice(newlineIndex + 1);
newlineIndex = buffer.indexOf("\n");
}
};
stream.on("data", onData);
return () => stream.off("data", onData);
}

class SecurePiRpcClient extends RpcClient {
constructor(
private readonly secureOptions: RpcClientOptions,
private readonly providerOptions?: PosthogProviderOptions,
) {
super(secureOptions);
}

override async start(): Promise<void> {
const internals = this as unknown as RpcClientInternals;
if (internals.process) {
throw new Error("Pi RPC client is already started");
}

internals.exitError = null;
const args = ["--mode", "rpc"];
if (this.secureOptions.provider) {
args.push("--provider", this.secureOptions.provider);
}
if (this.secureOptions.model) {
args.push("--model", this.secureOptions.model);
}
if (this.secureOptions.args) {
args.push(...this.secureOptions.args);
}

const child = spawn(
process.execPath,
[this.secureOptions.cliPath ?? "dist/cli.js", ...args],
{
cwd: this.secureOptions.cwd,
env: safePiEnvironment(process.env),
stdio: ["pipe", "pipe", "pipe", "pipe"],
},
);
Comment on lines +73 to +81

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would we need ELECTRON_RUN_AS_NODE=1 here?

internals.process = child;

child.stderr?.on("data", (data: Buffer) => {
internals.stderr += data.toString();
process.stderr.write(data);
});
child.once("exit", (code, signal) => {
if (internals.process !== child) {
return;
}
const error = internals.createProcessExitError(code, signal);
internals.exitError = error;
internals.rejectPendingRequests(error);
});
child.once("error", (error) => {
if (internals.process !== child) {
return;
}
const processError = new Error(
`Agent process error: ${error.message}. Stderr: ${internals.stderr}`,
);
internals.exitError = processError;
internals.rejectPendingRequests(processError);
});
child.stdin?.on("error", (error) => {
const stdinError =
internals.exitError ??
new Error(
`Agent process stdin error: ${error.message}. Stderr: ${internals.stderr}`,
);
internals.exitError = stdinError;
internals.rejectPendingRequests(stdinError);
});
if (child.stdout) {
internals.stopReadingStdout = attachJsonlReader(child.stdout, (line) =>
internals.handleLine(line),
);
}

const bootstrapPipe = child.stdio[3] as Writable | null;
bootstrapPipe?.end(
JSON.stringify({ providerOptions: this.providerOptions }),
);

await new Promise((resolve) => setTimeout(resolve, 100));
if (child.exitCode !== null) {
throw (
internals.exitError ??
internals.createProcessExitError(child.exitCode, child.signalCode)
);
}
}
}

export type PiRpcClientOptions = Pick<RpcClientOptions, "cwd" | "model"> & {
providerOptions?: PosthogProviderOptions;
};

export function createPiRpcClient(
options: PiRpcClientOptions = {},
): PiRpcClient {
const { providerOptions, ...rpcOptions } = options;
return new SecurePiRpcClient(
{
...rpcOptions,
cliPath: fileURLToPath(new URL("./rpc-host.js", import.meta.url)),
provider: "posthog",
},
providerOptions,
);
}
49 changes: 49 additions & 0 deletions packages/agent/src/pi/rpc-environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const SAFE_ENVIRONMENT_KEYS = [
"APPDATA",
"COLORTERM",
"ComSpec",
"FORCE_COLOR",
"HOME",
"LANG",
"LC_ALL",
"LOCALAPPDATA",
"LOGNAME",
"NO_COLOR",
"PATH",
"PATHEXT",
"SHELL",
"SystemRoot",
"TEMP",
"TERM",
"TMP",
"TMPDIR",
"USER",
"USERPROFILE",
"WINDIR",
"XDG_CACHE_HOME",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_RUNTIME_DIR",
"XDG_STATE_HOME",
] as const;

export function safePiEnvironment(
source: NodeJS.ProcessEnv,
): Record<string, string> {
const environment: Record<string, string> = {};
for (const key of SAFE_ENVIRONMENT_KEYS) {
const value = source[key];
if (value !== undefined) {
environment[key] = value;
}
}
return environment;
}

export function sanitizePiHostEnvironment(): void {
const environment = safePiEnvironment(process.env);
for (const key of Object.keys(process.env)) {
delete process.env[key];
}
Object.assign(process.env, environment);
}
Loading
Loading