Add token refresh endpoints to agent server#2018
Conversation
Adds a `set_token` command (also aliased as `posthog/set_token`) that updates `GH_TOKEN` / `GITHUB_TOKEN` on the agent process, so Claude-based agents pick up a refreshed token without restart. Adds a localhost-only `POST /gh` route that runs an arbitrary `gh` CLI invocation with the agent's current env, giving isolated Codex sandboxes a way to apply the new token via a shell wrapper. Generated-By: PostHog Code Task-Id: 2cbfd9f1-a4ff-4d49-a526-1184b909a373
|
| }); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| const LOOPBACK_ADDRESSES = new Set([ | ||
| "127.0.0.1", | ||
| "::1", | ||
| "::ffff:127.0.0.1", | ||
| "localhost", | ||
| ]); | ||
|
|
||
| export function isLoopbackAddress(address: string | undefined): boolean { |
There was a problem hiding this comment.
isLoopbackAddress only recognises a four-value fixed set, not the full loopback range
IPv4 reserves the entire 127.0.0.0/8 subnet as loopback, and IPv4-mapped loopback in IPv6 covers ::ffff:127.0.0.0/104. The set currently contains only 127.0.0.1 and ::ffff:127.0.0.1, so a connection arriving on any other address in those ranges (e.g. 127.0.0.2, ::ffff:127.1.2.3) is silently rejected with 403. A safer check would be a proper prefix test for 127. and ::ffff:127..
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agent/src/server/gh-exec.ts
Line: 88-100
Comment:
**`isLoopbackAddress` only recognises a four-value fixed set, not the full loopback range**
IPv4 reserves the entire `127.0.0.0/8` subnet as loopback, and IPv4-mapped loopback in IPv6 covers `::ffff:127.0.0.0/104`. The set currently contains only `127.0.0.1` and `::ffff:127.0.0.1`, so a connection arriving on any other address in those ranges (e.g. `127.0.0.2`, `::ffff:127.1.2.3`) is silently rejected with 403. A safer check would be a proper prefix test for `127.` and `::ffff:127.`.
How can I resolve this? If you propose a fix, please make it concise.| } | ||
| }); | ||
|
|
||
| // Sandbox-local exec for `gh`. Codex agents run in an isolated child process | ||
| // whose environment is captured at spawn time, so refreshing GH_TOKEN in the | ||
| // agent server doesn't reach them. Their gh-wrapper script calls this route | ||
| // to run gh against the agent server's freshly-refreshed env. Loopback-only; | ||
| // intentionally not JWT-protected so the wrapper has nothing to forward. | ||
| app.post("/gh", async (c) => { | ||
| const remote = getConnInfo(c).remote.address; | ||
| if (!isLoopbackAddress(remote)) { | ||
| this.logger.warn("Rejected non-loopback /gh request", { remote }); | ||
| return c.json({ error: "Forbidden" }, 403); | ||
| } | ||
|
|
||
| const rawBody = await c.req.json().catch(() => null); | ||
| const parsed = ghRequestSchema.safeParse(rawBody); | ||
| if (!parsed.success) { | ||
| return c.json({ error: parsed.error.message }, 400); | ||
| } | ||
|
|
||
| const { args, cwd, timeoutMs } = parsed.data; | ||
| try { | ||
| const result = await runGh(args, { | ||
| cwd: cwd ?? this.config.repositoryPath ?? process.cwd(), | ||
| timeoutMs: timeoutMs ?? 60_000, | ||
| logger: this.logger, | ||
| }); | ||
| return c.json(result); | ||
| } catch (error) { | ||
| this.logger.error("Failed to run gh", error); | ||
| return c.json( | ||
| { | ||
| error: error instanceof Error ? error.message : "Unknown error", | ||
| }, | ||
| 500, | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| app.notFound((c) => { | ||
| return c.json({ error: "Not found" }, 404); |
There was a problem hiding this comment.
/gh executes any gh subcommand, not just token-refresh operations
The args array is unconstrained, so any local process can invoke gh secret set, gh auth logout, gh repo delete, or other destructive subcommands with the agent's inherited credentials. The loopback restriction limits exposure to co-located processes, but it's broader than the stated use case requires. Consider restricting args[0] to an allowlist of safe subcommands (e.g. auth) or documenting why full gh pass-through is intentionally required.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agent/src/server/agent-server.ts
Line: 453-494
Comment:
**`/gh` executes any `gh` subcommand, not just token-refresh operations**
The `args` array is unconstrained, so any local process can invoke `gh secret set`, `gh auth logout`, `gh repo delete`, or other destructive subcommands with the agent's inherited credentials. The loopback restriction limits exposure to co-located processes, but it's broader than the stated use case requires. Consider restricting `args[0]` to an allowlist of safe subcommands (e.g. `auth`) or documenting why full `gh` pass-through is intentionally required.
How can I resolve this? If you propose a fix, please make it concise.- isLoopbackAddress: match the full 127.0.0.0/8 range (and the IPv4-mapped ::ffff:127.0.0.0/104 prefix) instead of a fixed four-value set, so legitimate loopback connections on 127.0.0.x are not silently rejected with 403. - Parameterise the duplicated /gh 400-validation tests and the two set_token command tests with it.each to remove identical scaffolding. Generated-By: PostHog Code Task-Id: 2cbfd9f1-a4ff-4d49-a526-1184b909a373
|
Make sure to not allow the caller to specify the binary. |
Drops the binary option from runGh's public type so callers — including the /gh HTTP route — cannot specify which binary is executed. The route already only forwards args/cwd/timeoutMs from the body schema, but removing the option makes that guarantee structural rather than incidental. Tests now exercise the underlying spawnAndCollect helper directly when they need to inject a stand-in binary. Generated-By: PostHog Code Task-Id: 2cbfd9f1-a4ff-4d49-a526-1184b909a373
Summary
set_tokencommand (also aliased asposthog/set_token) to the agent server that updatesGH_TOKENandGITHUB_TOKENon the agent process so Claude-based agents pick up a refreshed token without restart.POST /ghroute that runs an arbitraryghCLI invocation using the agent's current env. Codex-based agents run in an isolated sandbox, so a shell-script wrapper hits this endpoint to apply the new token throughgh.gh-exechelper (runGh+isLoopbackAddress) that spawnsghdirectly (no shell), inherits the agent'sprocess.env, and supports timeouts. The/ghroute reuses it; tests exercise both happy/error paths.Test plan
pnpm --filter agent test— full agent server suite passes (149/149), including 15 newgh-exectests and 6 newagent-servertests covering bothset_tokenaliases,/ghbody validation, and that/ghdoes not require JWT.pnpm --filter agent typecheck— clean.POST /commandwith{"command":"set_token","params":{"token":"…"}}— verifyGH_TOKENupdates in the agent process.POST /ghwith the wrapper script — verifyghruns with the new token.