From 4bbe7a7bf0a4e86faf15d11724f459604af16035 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 07:24:09 +0200 Subject: [PATCH 1/5] fix(tasks): restore cloud artifact uploads Read the rotating PostHog API credential from the dedicated sandbox OAuth file while retaining legacy and process-environment fallbacks. Add regression coverage for the split sandbox credential layout. Generated-By: PostHog Code Task-Id: 833df099-31c5-4930-addb-305cc5fe2def --- .../agent/src/signed-commit-artefacts.test.ts | 43 +++++++++++++++++++ packages/agent/src/signed-commit-artefacts.ts | 19 +++++--- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/packages/agent/src/signed-commit-artefacts.test.ts b/packages/agent/src/signed-commit-artefacts.test.ts index 1e33564942..96ea298270 100644 --- a/packages/agent/src/signed-commit-artefacts.test.ts +++ b/packages/agent/src/signed-commit-artefacts.test.ts @@ -1,7 +1,11 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { reportCommitArtefacts, reportTaskRunBranch, + resolveSandboxPosthogApi, } from "./signed-commit-artefacts"; const ENV = { @@ -13,6 +17,45 @@ const ENV = { // Point the env-file read at a path that never exists so only `env` is used. const NO_ENV_FILE = "/nonexistent/agent-env"; +describe("resolveSandboxPosthogApi", () => { + it("reads the rotating API key from the dedicated OAuth file", async () => { + const directory = await mkdtemp( + path.join(tmpdir(), "sandbox-posthog-api-"), + ); + const envFilePath = path.join(directory, "agent-env"); + const oauthEnvFilePath = path.join(directory, "agent-oauth-env"); + + try { + await writeFile( + envFilePath, + "POSTHOG_API_URL=https://us.posthog.com\0POSTHOG_PROJECT_ID=7\0", + ); + await writeFile( + oauthEnvFilePath, + "POSTHOG_PERSONAL_API_KEY=pha_refreshed\0", + ); + + expect( + resolveSandboxPosthogApi({}, envFilePath, oauthEnvFilePath), + ).toEqual({ + apiUrl: "https://us.posthog.com", + apiKey: "pha_refreshed", + projectId: 7, + }); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + + it("falls back to the process environment without credential files", () => { + expect(resolveSandboxPosthogApi(ENV, NO_ENV_FILE, NO_ENV_FILE)).toEqual({ + apiUrl: "https://us.posthog.com", + apiKey: "pha_test", + projectId: 7, + }); + }); +}); + const RESULT = { branch: "posthog-code/fix-foo", repository: "posthog/posthog", diff --git a/packages/agent/src/signed-commit-artefacts.ts b/packages/agent/src/signed-commit-artefacts.ts index 0d32f4bfdf..5700d7a72b 100644 --- a/packages/agent/src/signed-commit-artefacts.ts +++ b/packages/agent/src/signed-commit-artefacts.ts @@ -3,6 +3,7 @@ import type { SignedCommitResult } from "@posthog/git/signed-commit"; import { PostHogAPIClient } from "./posthog-api"; const SANDBOX_ENV_FILE = "/tmp/agent-env"; +const SANDBOX_OAUTH_ENV_FILE = "/tmp/agent-oauth-env"; /** * Best-effort "commit hook": after a successful signed-commit push, record one `commit` @@ -11,11 +12,10 @@ const SANDBOX_ENV_FILE = "/tmp/agent-env"; * endpoint reads the `X-PostHog-Task-Id` header, never the model. * * Credentials come from the sandbox environment (`POSTHOG_API_URL` / - * `POSTHOG_PERSONAL_API_KEY` / `POSTHOG_PROJECT_ID`), preferring the live agentsh env file - * for the key so a mid-session token refresh is picked up — the same pattern as - * `resolveGithubToken`. Works identically from the Claude in-process server and the Codex - * stdio child (both inherit the sandbox env). Never throws: a failed artefact post must not - * fail the commit that already landed. + * `POSTHOG_PERSONAL_API_KEY` / `POSTHOG_PROJECT_ID`), preferring the dedicated live agentsh + * OAuth file for the key so a mid-session token refresh is picked up — the same pattern as + * `resolveGithubToken`. Never throws: a failed artefact post must not fail the commit that + * already landed. */ interface SandboxPosthogApi { @@ -44,11 +44,15 @@ function readSandboxEnvFile(envFilePath: string): Record { export function resolveSandboxPosthogApi( env: Record = process.env, envFilePath: string = SANDBOX_ENV_FILE, + oauthEnvFilePath: string = SANDBOX_OAUTH_ENV_FILE, ): SandboxPosthogApi | undefined { const fileEnv = readSandboxEnvFile(envFilePath); + const oauthFileEnv = readSandboxEnvFile(oauthEnvFilePath); const apiUrl = fileEnv.POSTHOG_API_URL ?? env.POSTHOG_API_URL; const apiKey = - fileEnv.POSTHOG_PERSONAL_API_KEY ?? env.POSTHOG_PERSONAL_API_KEY; + oauthFileEnv.POSTHOG_PERSONAL_API_KEY ?? + fileEnv.POSTHOG_PERSONAL_API_KEY ?? + env.POSTHOG_PERSONAL_API_KEY; const projectId = Number( fileEnv.POSTHOG_PROJECT_ID ?? env.POSTHOG_PROJECT_ID, ); @@ -61,8 +65,9 @@ export function resolveSandboxPosthogApi( export function createSandboxPosthogClient( env?: Record, envFilePath?: string, + oauthEnvFilePath?: string, ): PostHogAPIClient | undefined { - const api = resolveSandboxPosthogApi(env, envFilePath); + const api = resolveSandboxPosthogApi(env, envFilePath, oauthEnvFilePath); if (!api) { return undefined; } From 216282ab0ac28f3f47d75415ab00ca143bd40900 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 07:32:41 +0200 Subject: [PATCH 2/5] fix(tasks): fail closed on revoked OAuth credentials Treat the dedicated OAuth credential file as authoritative when present. Empty or unreadable managed files now prevent fallback to stale launch-time credentials, matching the sandbox credential revocation contract. Generated-By: PostHog Code Task-Id: 833df099-31c5-4930-addb-305cc5fe2def --- .../agent/src/signed-commit-artefacts.test.ts | 42 +++++++++++++++++++ packages/agent/src/signed-commit-artefacts.ts | 24 ++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/agent/src/signed-commit-artefacts.test.ts b/packages/agent/src/signed-commit-artefacts.test.ts index 96ea298270..c45af63a93 100644 --- a/packages/agent/src/signed-commit-artefacts.test.ts +++ b/packages/agent/src/signed-commit-artefacts.test.ts @@ -54,6 +54,48 @@ describe("resolveSandboxPosthogApi", () => { projectId: 7, }); }); + + it("does not resurrect a stale token when the OAuth file is empty", async () => { + const directory = await mkdtemp( + path.join(tmpdir(), "sandbox-posthog-api-"), + ); + const envFilePath = path.join(directory, "agent-env"); + const oauthEnvFilePath = path.join(directory, "agent-oauth-env"); + + try { + await writeFile( + envFilePath, + "POSTHOG_API_URL=https://us.posthog.com\0POSTHOG_PERSONAL_API_KEY=pha_stale\0POSTHOG_PROJECT_ID=7\0", + ); + await writeFile(oauthEnvFilePath, ""); + + expect( + resolveSandboxPosthogApi(ENV, envFilePath, oauthEnvFilePath), + ).toBeUndefined(); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + + it("fails closed when the managed OAuth file is unreadable", async () => { + const directory = await mkdtemp( + path.join(tmpdir(), "sandbox-posthog-api-"), + ); + const envFilePath = path.join(directory, "agent-env"); + + try { + await writeFile( + envFilePath, + "POSTHOG_API_URL=https://us.posthog.com\0POSTHOG_PROJECT_ID=7\0", + ); + + expect( + resolveSandboxPosthogApi(ENV, envFilePath, directory), + ).toBeUndefined(); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); }); const RESULT = { diff --git a/packages/agent/src/signed-commit-artefacts.ts b/packages/agent/src/signed-commit-artefacts.ts index 5700d7a72b..17e69fb5ff 100644 --- a/packages/agent/src/signed-commit-artefacts.ts +++ b/packages/agent/src/signed-commit-artefacts.ts @@ -41,16 +41,36 @@ function readSandboxEnvFile(envFilePath: string): Record { } } +function readSandboxOauthToken(oauthEnvFilePath: string): string | undefined { + let raw: string; + try { + raw = readFileSync(oauthEnvFilePath, "utf8"); + } catch (error) { + // Only a missing file means the sandbox predates the dedicated credential + // channel. Any other read failure must fail closed instead of resurrecting + // the frozen launch-time token. + return (error as NodeJS.ErrnoException).code === "ENOENT" ? undefined : ""; + } + + // The backend revokes OAuth access by truncating this managed file. Its + // presence is authoritative even when empty. + if (raw.trim() === "") { + return ""; + } + const oauthEnv = readSandboxEnvFile(oauthEnvFilePath); + return oauthEnv.POSTHOG_PERSONAL_API_KEY ?? ""; +} + export function resolveSandboxPosthogApi( env: Record = process.env, envFilePath: string = SANDBOX_ENV_FILE, oauthEnvFilePath: string = SANDBOX_OAUTH_ENV_FILE, ): SandboxPosthogApi | undefined { const fileEnv = readSandboxEnvFile(envFilePath); - const oauthFileEnv = readSandboxEnvFile(oauthEnvFilePath); + const oauthToken = readSandboxOauthToken(oauthEnvFilePath); const apiUrl = fileEnv.POSTHOG_API_URL ?? env.POSTHOG_API_URL; const apiKey = - oauthFileEnv.POSTHOG_PERSONAL_API_KEY ?? + oauthToken ?? fileEnv.POSTHOG_PERSONAL_API_KEY ?? env.POSTHOG_PERSONAL_API_KEY; const projectId = Number( From 37f780d38c7e16e199fffa918a36ec06cab47a45 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 07:39:54 +0200 Subject: [PATCH 3/5] fix(tasks): require managed OAuth credentials Remove legacy and process-token fallbacks for sandbox PostHog authentication. Artifact and task metadata clients now require the dedicated OAuth credential file and fail closed when it is missing, empty, or unreadable. Generated-By: PostHog Code Task-Id: 833df099-31c5-4930-addb-305cc5fe2def --- .../agent/src/signed-commit-artefacts.test.ts | 37 +++++++++++++++---- packages/agent/src/signed-commit-artefacts.ts | 31 +++++++++------- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/packages/agent/src/signed-commit-artefacts.test.ts b/packages/agent/src/signed-commit-artefacts.test.ts index c45af63a93..0db9f0fa6a 100644 --- a/packages/agent/src/signed-commit-artefacts.test.ts +++ b/packages/agent/src/signed-commit-artefacts.test.ts @@ -1,7 +1,16 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; import { reportCommitArtefacts, reportTaskRunBranch, @@ -16,6 +25,18 @@ const ENV = { // Point the env-file read at a path that never exists so only `env` is used. const NO_ENV_FILE = "/nonexistent/agent-env"; +const TEST_OAUTH_ENV_FILE = path.join( + tmpdir(), + `posthog-agent-oauth-env-${process.pid}`, +); + +beforeAll(async () => { + await writeFile(TEST_OAUTH_ENV_FILE, "POSTHOG_PERSONAL_API_KEY=pha_test\0"); +}); + +afterAll(async () => { + await rm(TEST_OAUTH_ENV_FILE, { force: true }); +}); describe("resolveSandboxPosthogApi", () => { it("reads the rotating API key from the dedicated OAuth file", async () => { @@ -47,12 +68,10 @@ describe("resolveSandboxPosthogApi", () => { } }); - it("falls back to the process environment without credential files", () => { - expect(resolveSandboxPosthogApi(ENV, NO_ENV_FILE, NO_ENV_FILE)).toEqual({ - apiUrl: "https://us.posthog.com", - apiKey: "pha_test", - projectId: 7, - }); + it("fails closed without the dedicated OAuth file", () => { + expect( + resolveSandboxPosthogApi(ENV, NO_ENV_FILE, NO_ENV_FILE), + ).toBeUndefined(); }); it("does not resurrect a stale token when the OAuth file is empty", async () => { @@ -144,6 +163,7 @@ describe("reportCommitArtefacts", () => { message: "fix: foo", env: ENV, envFilePath: NO_ENV_FILE, + oauthEnvFilePath: TEST_OAUTH_ENV_FILE, }); const lookupCalls = fetchMock.mock.calls.filter(([url]) => @@ -202,6 +222,7 @@ describe("reportCommitArtefacts", () => { message: "fix: foo", env: ENV, envFilePath: NO_ENV_FILE, + oauthEnvFilePath: TEST_OAUTH_ENV_FILE, }), ).resolves.toBeUndefined(); }); @@ -225,6 +246,7 @@ describe("reportCommitArtefacts", () => { message: "fix: foo", env: ENV, envFilePath: NO_ENV_FILE, + oauthEnvFilePath: TEST_OAUTH_ENV_FILE, }); // Both commits attempted despite the first failing. @@ -260,6 +282,7 @@ describe("reportTaskRunBranch", () => { branch: "posthog-code/fix-foo", env: ENV, envFilePath: NO_ENV_FILE, + oauthEnvFilePath: TEST_OAUTH_ENV_FILE, }); expect(fetchMock).toHaveBeenCalledOnce(); diff --git a/packages/agent/src/signed-commit-artefacts.ts b/packages/agent/src/signed-commit-artefacts.ts index 17e69fb5ff..5ac284ad9a 100644 --- a/packages/agent/src/signed-commit-artefacts.ts +++ b/packages/agent/src/signed-commit-artefacts.ts @@ -45,11 +45,10 @@ function readSandboxOauthToken(oauthEnvFilePath: string): string | undefined { let raw: string; try { raw = readFileSync(oauthEnvFilePath, "utf8"); - } catch (error) { - // Only a missing file means the sandbox predates the dedicated credential - // channel. Any other read failure must fail closed instead of resurrecting - // the frozen launch-time token. - return (error as NodeJS.ErrnoException).code === "ENOENT" ? undefined : ""; + } catch { + // The dedicated credential channel is mandatory. Missing and unreadable + // files both fail closed. + return undefined; } // The backend revokes OAuth access by truncating this managed file. Its @@ -69,17 +68,13 @@ export function resolveSandboxPosthogApi( const fileEnv = readSandboxEnvFile(envFilePath); const oauthToken = readSandboxOauthToken(oauthEnvFilePath); const apiUrl = fileEnv.POSTHOG_API_URL ?? env.POSTHOG_API_URL; - const apiKey = - oauthToken ?? - fileEnv.POSTHOG_PERSONAL_API_KEY ?? - env.POSTHOG_PERSONAL_API_KEY; const projectId = Number( fileEnv.POSTHOG_PROJECT_ID ?? env.POSTHOG_PROJECT_ID, ); - if (!apiUrl || !apiKey || !Number.isFinite(projectId) || projectId <= 0) { + if (!apiUrl || !oauthToken || !Number.isFinite(projectId) || projectId <= 0) { return undefined; } - return { apiUrl, apiKey, projectId }; + return { apiUrl, apiKey: oauthToken, projectId }; } export function createSandboxPosthogClient( @@ -105,13 +100,18 @@ export async function reportCommitArtefacts(opts: { message: string; env?: Record; envFilePath?: string; + oauthEnvFilePath?: string; }): Promise { const { taskId, result, message } = opts; if (!taskId) { return; // Local/desktop run — no task to attribute or associate through. } try { - const client = createSandboxPosthogClient(opts.env, opts.envFilePath); + const client = createSandboxPosthogClient( + opts.env, + opts.envFilePath, + opts.oauthEnvFilePath, + ); if (!client) { return; // No sandbox PostHog credentials — nothing to report to. } @@ -146,12 +146,17 @@ export async function reportTaskRunBranch(opts: { branch: string; env?: Record; envFilePath?: string; + oauthEnvFilePath?: string; }): Promise { if (!opts.taskId || !opts.taskRunId) { return; } try { - const client = createSandboxPosthogClient(opts.env, opts.envFilePath); + const client = createSandboxPosthogClient( + opts.env, + opts.envFilePath, + opts.oauthEnvFilePath, + ); if (!client) { return; } From 93fc7d088f2706578f1a2e06080b97bcccd0f499 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 12:14:05 +0200 Subject: [PATCH 4/5] fix(tasks): refresh managed OAuth credentials per request Read the managed OAuth file from a single snapshot for each credential resolution. Re-resolve credentials for every request and authentication retry so rotation and revocation take effect without restarting the agent. Generated-By: PostHog Code Task-Id: 833df099-31c5-4930-addb-305cc5fe2def --- .../agent/src/signed-commit-artefacts.test.ts | 77 +++++++++++++++++++ packages/agent/src/signed-commit-artefacts.ts | 23 +++--- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/packages/agent/src/signed-commit-artefacts.test.ts b/packages/agent/src/signed-commit-artefacts.test.ts index 0db9f0fa6a..6eb3eb46bc 100644 --- a/packages/agent/src/signed-commit-artefacts.test.ts +++ b/packages/agent/src/signed-commit-artefacts.test.ts @@ -191,6 +191,83 @@ describe("reportCommitArtefacts", () => { } }); + it("rereads the OAuth file when credentials rotate between requests", async () => { + const directory = await mkdtemp( + path.join(tmpdir(), "sandbox-posthog-api-"), + ); + const oauthEnvFilePath = path.join(directory, "agent-oauth-env"); + await writeFile(oauthEnvFilePath, "POSTHOG_PERSONAL_API_KEY=pha_initial\0"); + + try { + fetchMock.mockImplementationOnce(async () => { + await writeFile( + oauthEnvFilePath, + "POSTHOG_PERSONAL_API_KEY=pha_rotated\0", + ); + return jsonResponse({ results: [{ id: "report-1" }] }); + }); + fetchMock.mockImplementation(async () => + jsonResponse({ id: "artefact" }), + ); + + await reportCommitArtefacts({ + taskId: "task-1", + result: { ...RESULT, commits: [RESULT.commits[0]] }, + message: "fix: foo", + env: ENV, + envFilePath: NO_ENV_FILE, + oauthEnvFilePath, + }); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect( + (fetchMock.mock.calls[0]?.[1]?.headers as Headers).get("Authorization"), + ).toBe("Bearer pha_initial"); + expect( + (fetchMock.mock.calls[1]?.[1]?.headers as Headers).get("Authorization"), + ).toBe("Bearer pha_rotated"); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + + it("rereads the OAuth file when retrying an auth failure", async () => { + const directory = await mkdtemp( + path.join(tmpdir(), "sandbox-posthog-api-"), + ); + const oauthEnvFilePath = path.join(directory, "agent-oauth-env"); + await writeFile(oauthEnvFilePath, "POSTHOG_PERSONAL_API_KEY=pha_initial\0"); + + try { + fetchMock.mockImplementationOnce(async () => { + await writeFile( + oauthEnvFilePath, + "POSTHOG_PERSONAL_API_KEY=pha_rotated\0", + ); + return new Response(null, { status: 401 }); + }); + fetchMock.mockImplementationOnce(async () => + jsonResponse({ results: [] }), + ); + + await reportCommitArtefacts({ + taskId: "task-1", + result: RESULT, + message: "fix: foo", + env: ENV, + envFilePath: NO_ENV_FILE, + oauthEnvFilePath, + }); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect( + (fetchMock.mock.calls[1]?.[1]?.headers as Headers).get("Authorization"), + ).toBe("Bearer pha_rotated"); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + it("is a no-op without a task id", async () => { await reportCommitArtefacts({ taskId: undefined, diff --git a/packages/agent/src/signed-commit-artefacts.ts b/packages/agent/src/signed-commit-artefacts.ts index 5ac284ad9a..33ef30a1df 100644 --- a/packages/agent/src/signed-commit-artefacts.ts +++ b/packages/agent/src/signed-commit-artefacts.ts @@ -42,22 +42,20 @@ function readSandboxEnvFile(envFilePath: string): Record { } function readSandboxOauthToken(oauthEnvFilePath: string): string | undefined { - let raw: string; try { - raw = readFileSync(oauthEnvFilePath, "utf8"); + const raw = readFileSync(oauthEnvFilePath, "utf8"); + for (const entry of raw.split("\0")) { + const prefix = "POSTHOG_PERSONAL_API_KEY="; + if (entry.startsWith(prefix)) { + return entry.slice(prefix.length); + } + } + return ""; } catch { // The dedicated credential channel is mandatory. Missing and unreadable // files both fail closed. return undefined; } - - // The backend revokes OAuth access by truncating this managed file. Its - // presence is authoritative even when empty. - if (raw.trim() === "") { - return ""; - } - const oauthEnv = readSandboxEnvFile(oauthEnvFilePath); - return oauthEnv.POSTHOG_PERSONAL_API_KEY ?? ""; } export function resolveSandboxPosthogApi( @@ -89,7 +87,10 @@ export function createSandboxPosthogClient( return new PostHogAPIClient({ apiUrl: api.apiUrl, projectId: api.projectId, - getApiKey: () => api.apiKey, + getApiKey: () => + readSandboxOauthToken(oauthEnvFilePath ?? SANDBOX_OAUTH_ENV_FILE) ?? "", + refreshApiKey: () => + readSandboxOauthToken(oauthEnvFilePath ?? SANDBOX_OAUTH_ENV_FILE) ?? "", }); } From 77c4658dbb793e1e95d8a44812737e5a717a49d2 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Tue, 28 Jul 2026 12:23:53 +0200 Subject: [PATCH 5/5] refactor(tasks): share sandbox environment parsing Extract the NUL-delimited environment parser and use its parsed object for OAuth token lookup. Add direct coverage for normal, malformed, and equals-containing entries. Generated-By: PostHog Code Task-Id: 833df099-31c5-4930-addb-305cc5fe2def --- .../agent/src/signed-commit-artefacts.test.ts | 16 ++++++++++ packages/agent/src/signed-commit-artefacts.ts | 29 +++++++++---------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/agent/src/signed-commit-artefacts.test.ts b/packages/agent/src/signed-commit-artefacts.test.ts index 6eb3eb46bc..e05b264153 100644 --- a/packages/agent/src/signed-commit-artefacts.test.ts +++ b/packages/agent/src/signed-commit-artefacts.test.ts @@ -12,11 +12,27 @@ import { vi, } from "vitest"; import { + parseSandboxEnv, reportCommitArtefacts, reportTaskRunBranch, resolveSandboxPosthogApi, } from "./signed-commit-artefacts"; +describe("parseSandboxEnv", () => { + it("parses NUL-delimited entries into an object", () => { + expect(parseSandboxEnv("FIRST=one\0SECOND=two\0")).toEqual({ + FIRST: "one", + SECOND: "two", + }); + }); + + it("preserves equals signs in values and ignores malformed entries", () => { + expect( + parseSandboxEnv("TOKEN=header.payload=signature\0invalid\0=value\0"), + ).toEqual({ TOKEN: "header.payload=signature" }); + }); +}); + const ENV = { POSTHOG_API_URL: "https://us.posthog.com", POSTHOG_PERSONAL_API_KEY: "pha_test", diff --git a/packages/agent/src/signed-commit-artefacts.ts b/packages/agent/src/signed-commit-artefacts.ts index 33ef30a1df..d5690d6948 100644 --- a/packages/agent/src/signed-commit-artefacts.ts +++ b/packages/agent/src/signed-commit-artefacts.ts @@ -24,17 +24,20 @@ interface SandboxPosthogApi { projectId: number; } +export function parseSandboxEnv(raw: string): Record { + const env: Record = {}; + for (const entry of raw.split("\0")) { + const separator = entry.indexOf("="); + if (separator > 0) { + env[entry.slice(0, separator)] = entry.slice(separator + 1); + } + } + return env; +} + function readSandboxEnvFile(envFilePath: string): Record { try { - const raw = readFileSync(envFilePath, "utf8"); - const env: Record = {}; - for (const entry of raw.split("\0")) { - const eq = entry.indexOf("="); - if (eq > 0) { - env[entry.slice(0, eq)] = entry.slice(eq + 1); - } - } - return env; + return parseSandboxEnv(readFileSync(envFilePath, "utf8")); } catch { // No env file (local/desktop or test) — fall back to the process env only. return {}; @@ -44,13 +47,7 @@ function readSandboxEnvFile(envFilePath: string): Record { function readSandboxOauthToken(oauthEnvFilePath: string): string | undefined { try { const raw = readFileSync(oauthEnvFilePath, "utf8"); - for (const entry of raw.split("\0")) { - const prefix = "POSTHOG_PERSONAL_API_KEY="; - if (entry.startsWith(prefix)) { - return entry.slice(prefix.length); - } - } - return ""; + return parseSandboxEnv(raw).POSTHOG_PERSONAL_API_KEY ?? ""; } catch { // The dedicated credential channel is mandatory. Missing and unreadable // files both fail closed.