Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/code/electron-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const config: Configuration = {
".vite/build/plugins/posthog/**",
".vite/build/codex-acp/**",
".vite/build/grammars/**",
".vite/build/rpc-host.js",
".vite/build/rpc-host.js.map",
...asarUnpackGlobs,
],

Expand Down
2 changes: 2 additions & 0 deletions apps/code/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
copyCodexAcpBinaries,
copyDrizzleMigrations,
copyEnricherGrammars,
copyPiRpcHost,
copyPosthogPlugin,
fixFilenameCircularRef,
getBuildDate,
Expand Down Expand Up @@ -93,6 +94,7 @@ export default defineConfig(({ mode }) => {
autoServicesPlugin(path.join(__dirname, "src/main/services")),
fixFilenameCircularRef(),
copyClaudeExecutable(),
copyPiRpcHost(),
copyPosthogPlugin(isDev),
copyDrizzleMigrations(),
copyCodexAcpBinaries(),
Expand Down
1 change: 1 addition & 0 deletions apps/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
"reflect-metadata": "^0.2.2",
"semver": "^7.8.1",
"shadcn": "^4.1.2",
"superjson": "catalog:",
"smol-toml": "^1.6.1",
"tailwindcss-scroll-mask": "^0.0.3",
"tw-animate-css": "^1.4.0",
Expand Down
27 changes: 22 additions & 5 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ import { OAUTH_CALLBACK_SERVER } from "@posthog/workspace-server/services/oauth-
import { oauthCallbackModule } from "@posthog/workspace-server/services/oauth-callback/oauth-callback.module";
import { onboardingImportModule } from "@posthog/workspace-server/services/onboarding-import/onboarding-import.module";
import { osModule } from "@posthog/workspace-server/services/os/os.module";
import { PI_SESSION_SERVICE } from "@posthog/workspace-server/services/pi-session/identifiers";
import type { PiSessionService } from "@posthog/workspace-server/services/pi-session/pi-session";
import { piSessionModule } from "@posthog/workspace-server/services/pi-session/pi-session.module";
import { POSTHOG_PLUGIN_SERVICE } from "@posthog/workspace-server/services/posthog-plugin/identifiers";
import { posthogPluginModule } from "@posthog/workspace-server/services/posthog-plugin/posthog-plugin.module";
import { PROCESS_TRACKING_SERVICE } from "@posthog/workspace-server/services/process-tracking/identifiers";
Expand Down Expand Up @@ -356,6 +359,7 @@ container
.bind(MAIN_DEFAULT_ADDITIONAL_DIRECTORY_REPOSITORY)
.toService(DEFAULT_ADDITIONAL_DIRECTORY_REPOSITORY);
container.load(agentModule);
container.load(piSessionModule);
container.bind(AGENT_SLEEP_COORDINATOR).toService(MAIN_SLEEP_SERVICE);
container.bind(AGENT_MCP_APPS).toService(MCP_APPS_SERVICE);
container.bind(AGENT_REPO_FILES).toService(MAIN_FS_SERVICE);
Expand Down Expand Up @@ -391,8 +395,12 @@ container.bind(MCP_PROXY_AUTH).toDynamicValue((ctx) => {
});
container.load(archiveModule);
container.bind(ARCHIVE_SESSION_CANCELLER).toDynamicValue((ctx) => ({
cancelSessionsByTaskId: (taskId: string) =>
ctx.get<AgentService>(AGENT_SERVICE).cancelSessionsByTaskId(taskId),
cancelSessionsByTaskId: async (taskId: string) => {
await Promise.all([
ctx.get<AgentService>(AGENT_SERVICE).cancelSessionsByTaskId(taskId),
ctx.get<PiSessionService>(PI_SESSION_SERVICE).stop(taskId),
]);
},
}));
container.bind(ARCHIVE_FILE_WATCHER).toDynamicValue((ctx) => ({
stopWatching: async (worktreePath: string) => {
Expand All @@ -403,8 +411,12 @@ container.bind(ARCHIVE_FILE_WATCHER).toDynamicValue((ctx) => ({
}));
container.load(suspensionModule);
container.bind(SUSPENSION_SESSION_CANCELLER).toDynamicValue((ctx) => ({
cancelSessionsByTaskId: (taskId: string) =>
ctx.get<AgentService>(AGENT_SERVICE).cancelSessionsByTaskId(taskId),
cancelSessionsByTaskId: async (taskId: string) => {
await Promise.all([
ctx.get<AgentService>(AGENT_SERVICE).cancelSessionsByTaskId(taskId),
ctx.get<PiSessionService>(PI_SESSION_SERVICE).stop(taskId),
]);
},
}));
container.bind(SUSPENSION_FILE_WATCHER).toDynamicValue((ctx) => ({
stopWatching: async (worktreePath: string) => {
Expand Down Expand Up @@ -675,7 +687,12 @@ container.load(workspaceModule);
container.bind(WORKSPACE_AGENT).toDynamicValue((ctx): WorkspaceAgent => {
const agent = ctx.get<AgentService>(AGENT_SERVICE);
return {
cancelSessionsByTaskId: (taskId) => agent.cancelSessionsByTaskId(taskId),
cancelSessionsByTaskId: async (taskId) => {
await Promise.all([
agent.cancelSessionsByTaskId(taskId),
ctx.get<PiSessionService>(PI_SESSION_SERVICE).stop(taskId),
]);
},
onAgentFileActivity: (handler) =>
agent.on(AgentServiceEvent.AgentFileActivity, handler),
};
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/main/trpc/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { notificationRouter } from "@posthog/host-router/routers/notification.ro
import { oauthRouter } from "@posthog/host-router/routers/oauth.router";
import { onboardingImportRouter } from "@posthog/host-router/routers/onboarding-import.router";
import { osRouter } from "@posthog/host-router/routers/os.router";
import { piSessionRouter } from "@posthog/host-router/routers/pi-session.router";
import { processTrackingRouter } from "@posthog/host-router/routers/process-tracking.router";
import { provisioningRouter } from "@posthog/host-router/routers/provisioning.router";
import { secureStoreRouter } from "@posthog/host-router/routers/secure-store.router";
Expand Down Expand Up @@ -93,6 +94,7 @@ export const trpcRouter = router({
onboardingImport: onboardingImportRouter,
logs: logsRouter,
os: osRouter,
piSession: piSessionRouter,
processTracking: processTrackingRouter,
provisioning: provisioningRouter,
sleep: sleepRouter,
Expand Down
3 changes: 3 additions & 0 deletions apps/code/src/renderer/di/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ import {
GITHUB_CONNECT_CLIENT,
type GithubConnectClient,
} from "@posthog/core/onboarding/identifiers";
import { PI_RUNNER } from "@posthog/core/pi-runtime/identifiers";
import type { PiRunner } from "@posthog/core/pi-runtime/piRunner";
import {
type BundleLocalSkill,
CLOUD_ARTIFACT_BUNDLE_LOCAL_SKILL,
Expand Down Expand Up @@ -276,6 +278,7 @@ export interface RendererBindings {
[SHELL_PROCESS_READER]: ShellProcessReader;
[ANALYTICS_TRACKER]: AnalyticsTracker;
[TASK_CREATION_HOST]: ITaskCreationHost;
[PI_RUNNER]: PiRunner;
[TASK_CREATION_EFFECTS]: TaskCreationEffects;
[RENDERER_TASK_SERVICE]: TaskService;
[TASK_SERVICE]: TaskService;
Expand Down
4 changes: 4 additions & 0 deletions apps/code/src/renderer/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
import { LLM_GATEWAY_SERVICE } from "@posthog/core/llm-gateway/identifiers";
import type { LlmGatewayService } from "@posthog/core/llm-gateway/llm-gateway";
import type { LlmMessage } from "@posthog/core/llm-gateway/schemas";
import { PI_RUNNER } from "@posthog/core/pi-runtime/identifiers";
import type { PiRunner } from "@posthog/core/pi-runtime/piRunner";
import {
CLOUD_ARTIFACT_BUNDLE_LOCAL_SKILL,
CLOUD_ARTIFACT_READ_FILE_AS_BASE64,
Expand Down Expand Up @@ -149,6 +151,7 @@ import { trpcClient } from "@renderer/trpc";
import { hostTrpcClient } from "@renderer/trpc/client";
import type { TRPCClient } from "@trpc/client";
import { hostLog, logger } from "@utils/logger";
import { TrpcPiRunner } from "../platform-adapters/trpc-pi-runner";
import type { RendererBindings } from "./bindings";
import { TASK_SERVICE as RENDERER_TASK_SERVICE, TRPC_CLIENT } from "./tokens";

Expand Down Expand Up @@ -288,6 +291,7 @@ container

// Bind services
container.bind<ITaskCreationHost>(TASK_CREATION_HOST).to(TrpcTaskCreationHost);
container.bind<PiRunner>(PI_RUNNER).to(TrpcPiRunner);
container.bind(TASK_CREATION_EFFECTS).toConstantValue(taskCreationEffects);
container.bind<TaskService>(RENDERER_TASK_SERVICE).to(TaskService);
container.bind<TaskService>(TASK_SERVICE).toService(RENDERER_TASK_SERVICE);
Expand Down
28 changes: 28 additions & 0 deletions apps/code/src/renderer/platform-adapters/trpc-pi-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type {
PiResumeInput,
PiRunInput,
PiRunner,
} from "@posthog/core/pi-runtime/piRunner";
import { resolveService } from "@posthog/di/container";
import {
HOST_TRPC_CLIENT,
type HostTrpcClient,
} from "@posthog/host-router/client";

function hostClient(): HostTrpcClient {
return resolveService<HostTrpcClient>(HOST_TRPC_CLIENT);
}

export class TrpcPiRunner implements PiRunner {
async create(input: PiRunInput): Promise<void> {
await hostClient().piSession.start.mutate(input);
}

resume(input: PiResumeInput): Promise<void> {
return hostClient().piSession.resume.mutate(input);
}

stop(taskId: string): Promise<void> {
return hostClient().piSession.stop.mutate({ taskId });
}
}
8 changes: 6 additions & 2 deletions apps/code/src/renderer/trpc/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import {
createTRPCOptionsProxy,
} from "@trpc/tanstack-react-query";
import { queryClient } from "@utils/queryClient";
import superjson from "superjson";
import type { TrpcRouter } from "../../main/trpc/router";

export const trpcClient = createTRPCClient<TrpcRouter>({
links: [ipcInstrumentationLink<TrpcRouter>(), ipcLink()],
links: [
ipcInstrumentationLink<TrpcRouter>(),
ipcLink({ transformer: superjson }),
],
});

export const hostTrpcClient = createTRPCClient<HostRouter>({
links: [ipcLink()],
links: [ipcLink({ transformer: superjson })],
});

const context = createTRPCContext<TrpcRouter>();
Expand Down
23 changes: 23 additions & 0 deletions apps/code/vite-main-plugins.mts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,29 @@ function copyClaudeSupportAssets(sourcePath: string, destDir: string): void {
}
}

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 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 ")}`,
);
}
copyFileSync(source, join(__dirname, ".vite/build/rpc-host.js"));
Comment thread
jonathanlab marked this conversation as resolved.
},
};
}

export function copyClaudeExecutable(): Plugin {
return {
name: "copy-claude-executable",
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@posthog/workspace-client": "workspace:*",
"@tanstack/react-query": "^5.100.14",
"@trpc/client": "^11.17.0",
"superjson": "catalog:",
"@trpc/tanstack-react-query": "^11.17.0",
"inversify": "^7.10.6",
"react": "19.2.6",
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/web-trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
httpSubscriptionLink,
splitLink,
} from "@trpc/client";
import superjson from "superjson";

// The ENTIRE electron->web transport difference. The renderer builds the same
// client with `links: [ipcLink()]`; web swaps in HTTP: httpBatchLink for
Expand All @@ -16,8 +17,8 @@ export const hostTrpcClient = createTRPCClient<HostRouter>({
links: [
splitLink({
condition: (op) => op.type === "subscription",
true: httpSubscriptionLink({ url: API_URL }),
false: httpBatchLink({ url: API_URL }),
true: httpSubscriptionLink({ url: API_URL, transformer: superjson }),
false: httpBatchLink({ url: API_URL, transformer: superjson }),
}),
],
});
4 changes: 4 additions & 0 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
"types": "./dist/posthog-products.d.ts",
"import": "./dist/posthog-products.js"
},
"./pi/rpc-client": {
"types": "./dist/pi/rpc-client.d.ts",
"import": "./dist/pi/rpc-client.js"
},
"./pr-url-detector": {
"types": "./dist/pr-url-detector.d.ts",
"import": "./dist/pr-url-detector.js"
Expand Down
14 changes: 12 additions & 2 deletions packages/agent/src/pi/rpc-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ describe("createPiRpcClient", () => {
const client = createPiRpcClient({
cwd: "/workspace",
model: "claude-opus-4-8",
apiKey: "token",
oauthCredentials: {
access: "access-token",
refresh: "refresh-token",
expires: 0,
region: "us",
},
env: { POSTHOG_REGION: "us" },
});

Expand All @@ -17,7 +22,12 @@ describe("createPiRpcClient", () => {
cwd: "/workspace",
model: "claude-opus-4-8",
env: {
POSTHOG_API_KEY: "token",
POSTHOG_OAUTH_CREDENTIALS: JSON.stringify({
access: "access-token",
refresh: "refresh-token",
expires: 0,
region: "us",
}),
POSTHOG_REGION: "us",
},
provider: "posthog",
Expand Down
46 changes: 29 additions & 17 deletions packages/agent/src/pi/rpc-client.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,49 @@
import type { ChildProcess } from "node:child_process";
import { fileURLToPath } from "node:url";
import {
RpcClient,
type RpcClientOptions,
} from "@earendil-works/pi-coding-agent";
import type { PosthogOAuthCredentials } from "@posthog/harness";

export type PiRpcClient = RpcClient;

type RpcClientProcessAccess = {
process?: ChildProcess;
};

// HACK: Pi RpcClient does not expose its spawned child process or PID publicly.
export function getPiRpcClientProcess(
client: PiRpcClient,
): ChildProcess | null {
return (client as unknown as RpcClientProcessAccess).process ?? null;
}

export type PiRpcClientOptions = Pick<
RpcClientOptions,
"cwd" | "env" | "model"
> & {
/** PostHog token passed only to the isolated RPC host process. */
apiKey?: string;
sessionFile?: string;
oauthCredentials: PosthogOAuthCredentials;
};

/**
* Create a native Pi RPC client backed by the PostHog harness.
*
* The client owns an isolated child process. Call `start()` before sending
* commands and `stop()` when the run is finished.
*/
export function createPiRpcClient(
options: PiRpcClientOptions = {},
): PiRpcClient {
const { apiKey, env, ...rpcOptions } = options;
export function createPiRpcClient(options: PiRpcClientOptions): PiRpcClient {
const { env, sessionFile, oauthCredentials, ...rpcOptions } = options;
const args = sessionFile ? ["--session-file", sessionFile] : undefined;
const cliPath = fileURLToPath(new URL("./rpc-host.js", import.meta.url));
const oauthEnvironment: Record<string, string> = {
POSTHOG_OAUTH_CREDENTIALS: JSON.stringify(oauthCredentials),
};
const childEnvironment: Record<string, string> = {
...env,
...oauthEnvironment,
};

Comment on lines +34 to 41

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.

P1 security Child Credentials Diverge After Refresh

The child receives a one-time copy of the desktop access and refresh tokens. If the provider rotates refresh tokens and the parent refreshes first, the long-running child retains the invalidated token and its next refresh fails even though the desktop remains signed in, interrupting the Pi session.

return new RpcClient({
...rpcOptions,
cliPath: fileURLToPath(new URL("./rpc-host.js", import.meta.url)),
env: {
...env,
...(apiKey ? { POSTHOG_API_KEY: apiKey } : {}),
},
args,
cliPath,
env: childEnvironment,
provider: "posthog",
});
}
Loading
Loading