Skip to content

Commit fbfffb6

Browse files
committed
fix(agent): treat an emptied sandbox env file as GitHub logout
resolveGithubToken prefers the live /tmp/agent-env file (which the backend rewrites when the sandbox's actor changes mid-session) but fell back to the process env whenever the file yielded no token. On an actor transition the backend logs the sandbox out by emptying the token vars in that file, so the fallback resurrected the previous actor's token — frozen in the agent-server's launch-time process env — letting a follow-up actor push as the prior actor. Distinguish a file that carries the token vars but empties them (an explicit logout: return "") from a file that never had them (unmanaged: defer to the process env). Only the latter falls back.
1 parent d942c02 commit fbfffb6

2 files changed

Lines changed: 54 additions & 12 deletions

File tree

packages/agent/src/utils/github-token.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ describe("github-token", () => {
5353
const path = writeEnvFile("GH_TOKEN=\0GITHUB_TOKEN=ghs_real\0");
5454
expect(readGithubTokenFromSandboxEnvFile(path)).toBe("ghs_real");
5555
});
56+
57+
it("returns '' (explicit logout) when every token var is present but empty", () => {
58+
const path = writeEnvFile("PATH=/usr/bin\0GH_TOKEN=\0GITHUB_TOKEN=\0");
59+
expect(readGithubTokenFromSandboxEnvFile(path)).toBe("");
60+
});
61+
62+
it("returns undefined when the file carries no token var at all", () => {
63+
const path = writeEnvFile("PATH=/usr/bin\0HOME=/root\0");
64+
expect(readGithubTokenFromSandboxEnvFile(path)).toBeUndefined();
65+
});
5666
});
5767

5868
describe("resolveGithubToken", () => {
@@ -72,5 +82,20 @@ describe("github-token", () => {
7282
"ghs_fromprocess",
7383
);
7484
});
85+
86+
it("does not resurrect the process-env token after a logout (emptied file)", () => {
87+
// The backend logs the sandbox out by emptying the token vars in the file.
88+
// The frozen launch-time process env still holds the previous actor's
89+
// token; resolving must NOT fall back to it.
90+
vi.stubEnv("GH_TOKEN", "ghs_previous_actor");
91+
const path = writeEnvFile("GH_TOKEN=\0GITHUB_TOKEN=\0");
92+
expect(resolveGithubToken(path)).toBe("");
93+
});
94+
95+
it("falls back to the process env when the file carries no token var", () => {
96+
vi.stubEnv("GH_TOKEN", "ghs_fromprocess");
97+
const path = writeEnvFile("PATH=/usr/bin\0");
98+
expect(resolveGithubToken(path)).toBe("ghs_fromprocess");
99+
});
75100
});
76101
});

packages/agent/src/utils/github-token.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { readFileSync } from "node:fs";
2-
import { readGithubTokenFromEnv } from "@posthog/git/signed-commit";
2+
import {
3+
GITHUB_TOKEN_ENV_VARS,
4+
readGithubTokenFromEnv,
5+
} from "@posthog/git/signed-commit";
36

47
// helpers for resolving the in-sandbox GitHub token
58
// agentsh env file (NUL-delimited `key=value` pairs) that the PostHog backend
@@ -12,19 +15,33 @@ export const SANDBOX_ENV_FILE = "/tmp/agent-env";
1215
export function readGithubTokenFromSandboxEnvFile(
1316
envFilePath: string = SANDBOX_ENV_FILE,
1417
): string | undefined {
18+
let raw: string;
1519
try {
16-
const raw = readFileSync(envFilePath, "utf8");
17-
const env: Record<string, string> = {};
18-
for (const entry of raw.split("\0")) {
19-
const eq = entry.indexOf("=");
20-
if (eq > 0) {
21-
env[entry.slice(0, eq)] = entry.slice(eq + 1);
22-
}
23-
}
24-
// Reuse the shared token-var allowlist + precedence instead of hardcoding.
25-
return readGithubTokenFromEnv(env);
20+
raw = readFileSync(envFilePath, "utf8");
2621
} catch {
27-
// No env file (local/desktop or test) — fall back to the process env.
22+
// No env file (local/desktop or test) — signal "unmanaged" so the caller
23+
// falls back to the process env.
24+
return undefined;
25+
}
26+
const env: Record<string, string> = {};
27+
for (const entry of raw.split("\0")) {
28+
const eq = entry.indexOf("=");
29+
if (eq > 0) {
30+
env[entry.slice(0, eq)] = entry.slice(eq + 1);
31+
}
32+
}
33+
// A non-empty value wins by the shared token-var precedence.
34+
const token = readGithubTokenFromEnv(env);
35+
if (token) {
36+
return token;
37+
}
38+
// The file is the backend's live credential channel. If it carries the token
39+
// vars but they are emptied, that is an explicit logout on an actor
40+
// transition — return "" so the caller does NOT resurrect the previous
41+
// actor's token from the frozen launch-time process env. Only a file with no
42+
// token vars at all is "unmanaged" and defers to the process env.
43+
if (GITHUB_TOKEN_ENV_VARS.some((name) => name in env)) {
44+
return "";
2845
}
2946
return undefined;
3047
}

0 commit comments

Comments
 (0)