|
1 | 1 | # @posthog/harness |
2 | 2 |
|
3 | | -Spawn the [pi.dev](https://pi.dev) coding agent — both its **CLI** and its **SDK** — against the |
4 | | -PostHog LLM gateway, authenticated with the same OAuth flow as PostHog Code. |
5 | | - |
6 | | -Harness registers a pi provider named `posthog` that: |
7 | | - |
8 | | -- points pi's `anthropic-messages` API at the region's LLM gateway |
9 | | - (`https://gateway.<region>.posthog.com/posthog_code`), |
10 | | -- authenticates with a PostHog OAuth access token (`pha_…`), obtained through the same |
11 | | - Authorization-Code + PKCE flow PostHog Code uses (same client IDs, scopes, and `/oauth/authorize` |
12 | | - + `/oauth/token` endpoints from `@posthog/shared`), and |
13 | | -- lets pi own credential storage and refresh via its provider `oauth` hooks. |
14 | | - |
15 | | -Because the token, OAuth client, and gateway product (`posthog_code`) are identical to PostHog Code, |
16 | | -gateway results are identical as well. |
17 | | - |
18 | | -## Models |
19 | | - |
20 | | -The model list is fetched from the gateway's `/{product}/v1/models` at startup, so harness exposes |
21 | | -whatever models the gateway currently serves — including OpenAI + codex (`gpt-5.6-sol`, |
22 | | -`gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5`, `gpt-5.4`, `gpt-5.3-codex`, …) and GLM |
23 | | -(`@cf/zai-org/glm-5.2`). Each model is routed by owner: |
24 | | - |
25 | | -- Anthropic + Cloudflare/GLM models → pi's `anthropic-messages` API on `<gateway>/posthog_code` |
26 | | -- OpenAI + codex models → pi's `openai-responses` API on `<gateway>/posthog_code/v1` |
27 | | - |
28 | | -If the fetch fails, returns no models, or `PI_OFFLINE` / `HARNESS_STATIC_MODELS` is set, a bundled |
29 | | -fallback list is used instead. Select any model with `--model posthog/<id>` (e.g. |
30 | | -`--model posthog/gpt-5.3-codex`, `--model "posthog/@cf/zai-org/glm-5.2"`). |
31 | | - |
32 | | -## OAuth flow and region selection |
33 | | - |
34 | | -`harness /login` runs an Authorization-Code + PKCE flow: |
35 | | - |
36 | | -1. Determines the region: if `POSTHOG_REGION` (or an explicit `region` option) is set, it's used |
37 | | - directly; otherwise the login prompts interactively for the region to use, offering `United |
38 | | - States` and `European Union` (`dev` is not offered interactively — it's reachable only via |
39 | | - `POSTHOG_REGION=dev`). |
40 | | -2. Generates a PKCE code verifier/challenge (`S256`) and a random `state`. |
41 | | -3. Starts a loopback HTTP server on `127.0.0.1:<port>` at `/callback` |
42 | | - (port from `HARNESS_OAUTH_PORT`, default `8237`). |
43 | | -4. Builds the authorize URL for the resolved region with the same `client_id`, `scope`, and |
44 | | - `required_access_level=project` as PostHog Code, and opens it in the default browser. |
45 | | -5. Waits for the browser redirect to hit `/callback` with `code` and matching `state` (rejects on an |
46 | | - `error` param, missing `code`, a `state` mismatch, a 180s timeout, or cancellation). |
47 | | -6. Exchanges the code for tokens via `POST <cloudUrl>/oauth/token`. |
48 | | -7. Stores `OAuthCredentials` (`access`, `refresh`, `expires`, `region`) for pi to reuse and refresh. |
49 | | - |
50 | | -Token refresh posts `grant_type=refresh_token` to the same token endpoint, using the region stored in |
51 | | -the credentials. |
52 | | - |
53 | | -The provider also implements pi's `oauth.modifyModels` hook: whenever pi (re)loads models for this |
54 | | -provider — at startup with a previously-stored credential, and again immediately after a successful |
55 | | -login — it rewrites every model's `baseUrl` to match the region stored in that credential. This means |
56 | | -the region chosen at login always wins for routing requests, regardless of what region the provider |
57 | | -was initially registered with (e.g. before any login had happened). |
| 3 | +PostHog's [Pi](https://pi.dev) distribution. |
| 4 | + |
| 5 | +It adds the PostHog LLM provider and the extensions in `src/extensions/` while keeping Pi's native runtime, sessions, tools, and RPC protocol. |
58 | 6 |
|
59 | 7 | ## CLI |
60 | 8 |
|
61 | 9 | ```bash |
62 | | -harness # interactive pi, with the posthog provider available |
63 | | -harness /login # sign in via the PostHog OAuth flow; prompts for a region if none is set |
64 | | -harness -p "hi" --model posthog/claude-opus-4-8 |
| 10 | +hog |
| 11 | +hog /login |
| 12 | +hog -p "Fix the tests" --model posthog/claude-opus-4-8 |
65 | 13 | ``` |
66 | 14 |
|
67 | | -`POSTHOG_REGION` (`us` / `eu` / `dev`) is optional: if set, it's used directly (skipping the region |
68 | | -prompt at login) and the interactive prompt is skipped entirely. If unset, the initial (pre-login) |
69 | | -provider registration defaults to `us` for model discovery, and `/login` prompts for the actual |
70 | | -region to authenticate against — which then takes over routing via `modifyModels` above. The OAuth |
71 | | -loopback callback port can be overridden with `HARNESS_OAUTH_PORT` (default `8237`). |
| 15 | +`harness` is an alias for `hog`. |
72 | 16 |
|
73 | | -## Spawn the CLI as a subprocess |
| 17 | +## Runtime |
74 | 18 |
|
75 | 19 | ```ts |
76 | | -import { spawnPiCli } from "@posthog/harness/spawn"; |
| 20 | +import { createHarnessRuntime } from "@posthog/harness"; |
| 21 | + |
| 22 | +const runtime = await createHarnessRuntime({ cwd: "/workspace" }); |
77 | 23 |
|
78 | | -const child = spawnPiCli(["-p", "list the files", "--model", "posthog/claude-opus-4-8"], { |
79 | | - env: { POSTHOG_REGION: "us" }, |
80 | | -}); |
| 24 | +await runtime.session.prompt("Fix the tests"); |
81 | 25 | ``` |
82 | 26 |
|
83 | | -`spawnPiCli` launches the real `pi` binary with the PostHog provider loaded as an extension. |
| 27 | +`createHarnessRuntime()` returns Pi's native `AgentSessionRuntime`. |
84 | 28 |
|
85 | | -## SDK |
| 29 | +## RPC |
86 | 30 |
|
87 | 31 | ```ts |
88 | | -import { createHarnessSession } from "@posthog/harness/session"; |
89 | | - |
90 | | -const session = await createHarnessSession({ region: "us", model: "claude-opus-4-8" }); |
91 | | -session.subscribe((event) => { |
92 | | - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { |
93 | | - process.stdout.write(event.assistantMessageEvent.delta); |
94 | | - } |
95 | | -}); |
96 | | -await session.prompt("What files are in the current directory?"); |
| 32 | +import { createHarnessRuntime, runRpcMode } from "@posthog/harness"; |
| 33 | + |
| 34 | +const runtime = await createHarnessRuntime({ cwd: "/workspace" }); |
| 35 | +await runRpcMode(runtime); |
97 | 36 | ``` |
98 | 37 |
|
99 | | -The SDK reuses whatever credential `harness /login` stored, or accepts a static `apiKey` (a `pha_` |
100 | | -token) for headless use. |
101 | | - |
102 | | -## MCP servers |
103 | | - |
104 | | -The bundled `mcp` extension (see [`src/extensions/mcp/README.md`](./src/extensions/mcp/README.md)) |
105 | | -connects pi to [Model Context Protocol](https://modelcontextprotocol.io) servers and registers |
106 | | -their tools as pi tools named `mcp_<server>_<tool>`. |
107 | | - |
108 | | -Servers are configured in `mcp.json` — global (`~/.pi/agent/mcp.json`) and/or project-local |
109 | | -(`.pi/mcp.json`, honored only for trusted projects; project entries override global ones per key): |
110 | | - |
111 | | -```json |
112 | | -{ |
113 | | - "mcpServers": { |
114 | | - "filesystem": { |
115 | | - "command": "npx", |
116 | | - "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"] |
117 | | - }, |
118 | | - "linear": { |
119 | | - "transport": "streamable-http", |
120 | | - "url": "https://mcp.linear.app/mcp", |
121 | | - "auth": { "type": "oauth" } |
122 | | - } |
123 | | - } |
124 | | -} |
| 38 | +RPC is Pi's JSONL protocol over stdin/stdout. Harness does not define another protocol. |
| 39 | + |
| 40 | +## Authentication |
| 41 | + |
| 42 | +Run `hog /login`, or pass a PostHog personal API key: |
| 43 | + |
| 44 | +```ts |
| 45 | +const runtime = await createHarnessRuntime({ apiKey: "pha_…" }); |
125 | 46 | ``` |
126 | 47 |
|
127 | | -Supports stdio, streamable-http, and SSE transports; eager/lazy startup; automatic reconnect; |
128 | | -live tool refresh on `tools/list_changed`; static header auth; and full OAuth |
129 | | -(authorization-code + PKCE with discovery, dynamic client registration, silent token refresh, and |
130 | | -credentials stored under `~/.pi/agent/mcp-auth/`). Commands: `/mcp` (status), `/mcp:start`, |
131 | | -`/mcp:stop`, `/mcp:auth [server] [reset]` (browser flow). |
| 48 | +Set `POSTHOG_REGION` to `us`, `eu`, or `dev` when needed. |
132 | 49 |
|
133 | | -## Entry points |
| 50 | +## Public API |
134 | 51 |
|
135 | | -| Import | What | |
| 52 | +| Import | Purpose | |
136 | 53 | | --- | --- | |
137 | | -| `@posthog/harness/cli` (bin `harness`) | pi CLI in-process with the PostHog provider | |
138 | | -| `@posthog/harness/spawn` | `spawnPiCli()` — spawn pi as a subprocess | |
139 | | -| `@posthog/harness/session` | `createHarnessSession()` — pi SDK `AgentSession` | |
140 | | -| `@posthog/harness/extensions` | extension registry | |
141 | | -| `@posthog/harness/extensions/hog-branding` | startup header rebrand — `createHogBrandingExtension()` | |
142 | | -| `@posthog/harness/extensions/posthog-provider` | default pi extension — `createPosthogProviderExtension()` | |
143 | | -| `@posthog/harness/extensions/posthog-provider/provider` | `POSTHOG_PROVIDER_NAME`, `buildPosthogProvider()`, `resolvePosthogProvider()` | |
144 | | -| `@posthog/harness/extensions/posthog-provider/oauth` | `loginPosthog()`, `refreshPosthog()`, `buildAuthorizeUrl()`, `getRedirectUri()`, `getCallbackPort()` | |
145 | | -| `@posthog/harness/extensions/posthog-provider/gateway` | `getGatewayBaseUrl()`, `getLlmGatewayUrl()`, `resolveRegion()`, `GATEWAY_PRODUCT` | |
146 | | -| `@posthog/harness/extensions/posthog-provider/models` | `resolveModelConfigs()`, `fallbackModelConfigs()`, `DEFAULT_MODEL`, `GatewayModel` | |
147 | | -| `@posthog/harness/extensions/web-access` | web search + fetch tools — `createWebAccessExtension()` | |
148 | | -| `@posthog/harness/extensions/subagent` | subagent orchestration — `createSubagentExtension()` | |
149 | | -| `@posthog/harness/extensions/mcp` | MCP client extension — `createMcpExtension()` | |
| 54 | +| `@posthog/harness` | Runtime creation and RPC mode | |
| 55 | +| `@posthog/harness/runtime` | Runtime creation only | |
| 56 | +| `@posthog/harness/extensions` | Harness extension registry | |
| 57 | + |
| 58 | +Individual extensions are also exported under `@posthog/harness/extensions/*`. |
0 commit comments