mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 05:16:50 +01:00
fix(adapter-utils): provide a default author for local commits (#18094)
<!-- entire-trail-link-start --> https://entire.com/gh/paperclipai/-/trails/209/changes/paperclip/209 <!-- entire-trail-link-end --> ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agents use managed launchers for Git and GitHub commands. > - These launchers exclude host credentials and host Git identity. > - A checkout without an author cannot save a local commit when managed GitHub identity is absent. > - A local author default belongs in the launcher that owns this isolation. > - This pull request supplies a private Paperclip default and preserves explicit and managed identity selection. > - Agents can save local work without manual author setup or extra GitHub access. ## Linked Issues or Issue Description **What happened?** An ordinary `git commit` fails with `Author identity unknown` in a fresh checkout without author settings or a usable managed GitHub identity. An agent must set its own author to continue. **Expected behavior** The managed launcher supplies a default local author. Repository and command settings override that default. A current managed identity retains its existing precedence. **Steps to reproduce** Run the managed launcher in a fresh Git repository without author settings or broker credentials. Add a file, then run `git commit -m work`. **Paperclip version or commit** Reproduced at `063cfd43813cabf18417605fb9eefb9aacf95c8d` on Linux with Node 25.9.0 and pnpm 9.15.4. **Deployment mode** Managed sandbox execution. The shared launcher also serves local and SSH execution that selects managed access. Related: #16662 changes precedence for a connected GitHub account. #8945 assigns identity to managed worktrees. This repair supplies the missing anonymous launcher default and keeps the current managed precedence. It does not require either change. ### Hypothesis and thesis for a fix **Hypothesis:** The launcher removes inherited author variables and host Git config. Without a broker identity or explicit repository settings, it has no author. Four real-Git cases reproduce the reported error on the base revision. **Thesis for a fix:** Put `Paperclip <noreply@paperclip.ing>` in a private Git config for each operation. Git treats it as global config, so repository and command settings can override it. The existing broker environment still takes precedence. Do not store a person's identity in shared checkout config, where it could remain after steering or revocation. The default grants no GitHub access. ## What Changed - Write the default author to the existing private operation directory with mode `0600`. Remove it when the operation exits. - Point managed Git at this file while keeping host global/system config, credentials and SSH identity excluded. - Add real-Git regressions for missing, absent, unavailable and rejected access. Preserve explicit overrides, identity changes and revocation controls. - Verify two shell commits through normal launcher staging and a commit after anonymous heartbeat cleanup. Check saved files, commit metadata and temporary config cleanup. - Update identity documentation and the two run map recipes. ## Verification - Red: the four new local-commit cases fail on the base launcher with `Author identity unknown`. - Green: all 80 focused tests pass in one run across `github-launcher.test.ts`, `github-launcher-environment.test.ts`, `heartbeat-github-launchers.test.ts` and `native-github-access.test.ts` (23.31 seconds). - A controlled filesystem failure first made the candidate discard valid managed credentials. After the correction, the same real launcher process retains them. Default-author file failure does not change operation-directory readiness. - Review found a second failure: a partial write can leave invalid Git config. The real-process test failed before the repair with `fatal: bad config line` from `git status`. The launcher now selects the default file only after the write succeeds. Both write-failure cases retain managed credentials, allow status, require identity when no default is available, and save a commit with explicit identity. The test reads the stored commit metadata with real Git. - Real shells, generated launchers and Git create and inspect saved commits. Sandbox transport and broker replies use controlled fixtures. No live cloud rollout or GitHub write acceptance is claimed. - The isolated test process excludes inherited task launcher paths, Git config and broker authentication variables. An initial run exposed those test-environment inputs; the isolated replay passed. The heartbeat assertion now checks the private default file and its cleanup instead of expecting `/dev/null`. - Adapter-utils typecheck and build pass. Map validation and `git diff --check` pass. - Conflict repair on 2026-10-29: merged master `fa5e70faf47ef2ca20bf2daf95b7503c94469b89` into the PR branch. Resolved 19 conflict paths by retaining master for unrelated files. All seven Git identity files remain byte-for-byte equal to the reviewed fix. The final diff is seven files, 145 additions and 15 deletions. - At head `c501e3f28369d404c91082b89188dc150c9362a9`, all 80 focused tests pass again (37.53 seconds). Adapter-utils typecheck and build pass. Map validation and migration checks pass. The PR diff passes `git diff --check`. - Full workspace typecheck and build were attempted again. Both stop because this sandbox has no Rust `cargo`. The duplicate full local Vitest run was stopped after about five minutes without a final result while full CI was running. The earlier full local attempt was stopped without a final result. No local full-suite pass is claimed. - GitHub now reports the PR as mergeable. All current-head checks have completed. [Full CI run 40878973679](https://github.com/paperclipai/paperclip/actions/runs/40878973679) passes, including typecheck, build, Rust, test shards and all eight E2E shards. The exact-head check inventory has 54 successes, one neutral Paperclip Review and two Storybook skips. Snyk passes. Greptile and Paperclip each reviewed `c501e3f28369d404c91082b89188dc150c9362a9` at 5/5 with no actionable findings. All review threads are resolved. GitHub reports the PR as clean and mergeable. - The prior CI run had one failed E2E test: `a lost creation response requires reload and cannot blindly replay commands`. Its YAML parser received page text that starts with `Skip to Main Content`. The new run passes E2E shard 7/8 and the full E2E gate. The prior failure remains part of this record. A passing run does not establish the earlier failure's cause. This change does not alter that test or the agent configuration UI. ## Risks - Commits without explicit or managed identity now use the generic Paperclip author. Operators can still set repository or command identity. - `git config --global` writes to temporary operation config and does not persist. The documentation states this behavior. - If the private config directory cannot be prepared, commits still need explicit identity. Existing redacted diagnostics and credential isolation remain. - One small synchronous file write is added to a directory the launcher already creates. No new subprocess, network request, persistent identity or authority cache is added. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, shell execution and tool use. The runtime does not expose the exact model ID or context window size to this agent. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
018c4c9c9b
commit
7d8d1fbc1d
7 files changed
+145
-15
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# GitHub identity during agent execution
|
||||
|
||||
Shared agents use the GitHub connection of the person whose accepted instructions they are executing. Task ownership remains unchanged. GitHub is optional: ordinary work can start without a connection; a private checkout, authenticated API call, or commit can fail when that operation needs credentials or author metadata.
|
||||
Shared agents use the GitHub connection of the person whose accepted instructions they are executing. Task ownership remains unchanged. GitHub is optional: ordinary work can start without a connection. Managed launchers provide a default author for local commits; private checkouts and authenticated API calls still require eligible credentials.
|
||||
|
||||
## Accepted instructions and continuations
|
||||
|
||||
@@ -54,10 +54,17 @@ runner startup and provider startup. Shell operations acquire credentials for
|
||||
the active run through that broker. They do not require a persistent GitHub CLI
|
||||
login or inherit an ambient host broker capability.
|
||||
|
||||
Without a managed identity, local commits can use an explicitly configured
|
||||
repository identity or `git -c user.name=... -c user.email=...`. The launcher
|
||||
leaves author/committer environment variables unset and requires configured
|
||||
identity instead of guessing the host user's details. Managed shell profiles
|
||||
Without a managed identity, local commits default to
|
||||
`Paperclip <noreply@paperclip.ing>`. The launcher writes this token-free default
|
||||
to a private per-operation global Git configuration file, removed on exit.
|
||||
Repository identity and `git -c user.name=... -c user.email=...` override it.
|
||||
The launcher leaves author/committer environment variables unset instead of
|
||||
guessing the host user's details. `git config --global` changes only this
|
||||
temporary file; use repository configuration for persistent overrides.
|
||||
If the private configuration directory or default author file cannot be prepared,
|
||||
Git ignores the incomplete file. Local commits still require an explicit
|
||||
repository or command identity when no managed identity is available. A failed
|
||||
default author write does not remove valid managed credentials. Managed shell profiles
|
||||
remove empty identity overrides after environment merging, so agents do not
|
||||
need to unset them per command. A captured managed identity still takes
|
||||
precedence over repository configuration.
|
||||
|
||||
@@ -4,6 +4,18 @@ Product map: [runs-adapters](../feature-map/runs-adapters.md).
|
||||
|
||||
Feature ID: `runs-adapters`.
|
||||
|
||||
Scoped local Git author contract (`runs-adapters/runs`): an admitted execution
|
||||
with managed launchers can create a local commit without a GitHub grant. A
|
||||
private per-operation Git config supplies `Paperclip <noreply@paperclip.ing>`
|
||||
only as a default. Repository/command configuration and captured managed identity
|
||||
retain precedence. The default conveys no API, repository-network or credential
|
||||
authority; current run, company, trust and GitHub-grant checks stay in force.
|
||||
The [launcher controls](../../packages/adapter-utils/src/github-launcher.test.ts)
|
||||
exercise real Git with controlled broker responses; the
|
||||
[staging controls](../../packages/adapter-utils/src/github-launcher-environment.test.ts)
|
||||
exercise real local shells through a simulated sandbox transport. Neither
|
||||
qualifies live cloud execution or GitHub writes. No REST operation changes.
|
||||
|
||||
Delegated native entries inherit the [native admission and work-mode limits](README.md#native-rest-transport).
|
||||
|
||||
Review date: 2026-10-26. Source base: `a1f77821b9cd5c9c846fdf7a0fa5cc620ad2b6bb`; implementation: [#17737](https://github.com/paperclipai/paperclip/pull/17737).
|
||||
|
||||
@@ -40,6 +40,17 @@ The agent header has one Run now action. Administrators can enable
|
||||
Capture raw provider traces under the agent configuration’s Debugging section
|
||||
for future runs. The header shows Raw tracing on while that setting is enabled.
|
||||
|
||||
Managed Git launchers let agents save local commits without a GitHub connection
|
||||
or manual author setup. Verify a fresh checkout and a second shell in the same
|
||||
workspace: both commits should use `Paperclip <noreply@paperclip.ing>` when no
|
||||
identity is configured. Repository and command settings override this default;
|
||||
an eligible managed identity still takes precedence. Inspect stored commit
|
||||
metadata and confirm the default did not change repository or host configuration.
|
||||
Repeat with unavailable GitHub access; authenticated operations must still obey
|
||||
the existing credential checks. The [launcher controls](../../packages/adapter-utils/src/github-launcher.test.ts)
|
||||
and [real-shell staging controls](../../packages/adapter-utils/src/github-launcher-environment.test.ts)
|
||||
cover this local behavior, separately from live cloud execution and GitHub writes.
|
||||
|
||||
For latency diagnosis with an operator OTLP endpoint, search the hashed run ID
|
||||
without restricting the root span name. Compare workspace preparation with its
|
||||
`workspace_<step>` intervals. Measure controller readiness separately through
|
||||
|
||||
@@ -314,6 +314,29 @@ describe("managed GitHub launcher environment", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("saves local commits through staged sandbox shells without author setup", async () => {
|
||||
const fixture = await sandbox("usr/bin");
|
||||
await rm(path.join(fixture.bin, "git"));
|
||||
await writeFile(path.join(fixture.root, ".gitconfig"), "[user]\nname = Image User\nemail = image@example.test\n");
|
||||
const env = await prepareGitHubOperationLaunchers({
|
||||
runId: "default-local-author", target: fixture.target, cwd: fixture.root,
|
||||
env: githubBrokerEnvironment({}, { url: "", token: "" }),
|
||||
});
|
||||
const command = async (script: string) => {
|
||||
const result = await fixture.runner.execute({ command: "bash", args: ["--noprofile", "--norc", "-c", script], env });
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
return result.stdout.trim();
|
||||
};
|
||||
await command("git init -q && printf 'first\\n' > work.txt && git add work.txt && git commit -qm first");
|
||||
await command("printf 'second\\n' >> work.txt && git add work.txt && git commit -qm second");
|
||||
expect(await command("git log --format='%an <%ae>|%cn <%ce>'"))
|
||||
.toBe(Array(2).fill("Paperclip <noreply@paperclip.ing>|Paperclip <noreply@paperclip.ing>").join("\n"));
|
||||
expect(await command("git show HEAD:work.txt")).toBe("first\nsecond");
|
||||
expect(await readFile(path.join(fixture.root, ".git", "config"), "utf8")).not.toContain("[user]");
|
||||
expect(await readFile(path.join(fixture.root, ".gitconfig"), "utf8")).toContain("Image User");
|
||||
expect(await readdir(env.GH_CONFIG_DIR!)).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps task-only startup free of command probes and host credentials", async () => {
|
||||
const target = { kind: "remote" as const, transport: "sandbox" as const, providerKey: "plugin", remoteCwd: "/project", runnerTask: async () => ({}) };
|
||||
await expect(prepareGitHubExecutionEnvironment({ target, cwd: "/absent-controller-path", env: {}, hostCredentials: false, networkAccess: true }))
|
||||
|
||||
@@ -11,6 +11,37 @@ const cleanups: Array<() => Promise<unknown>> = [];
|
||||
afterEach(async () => { for (const cleanup of cleanups.splice(0).reverse()) await cleanup(); });
|
||||
|
||||
describe("managed GitHub launchers", () => {
|
||||
it.each(["missing", "absent", "unavailable", "rejected"])("commits local work without an identity when GitHub access is %s", async (access) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-github-default-author-"));
|
||||
cleanups.push(() => rm(root, { recursive: true, force: true }));
|
||||
const bin = path.join(root, "managed");
|
||||
await mkdir(bin);
|
||||
await exec("git", ["init", root]);
|
||||
await writeFile(path.join(bin, "git"), githubLauncherSource(), { mode: 0o700 });
|
||||
const server = createServer((_req, res) => {
|
||||
res.writeHead(access === "rejected" ? 403 : 200, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ status: access, env: {
|
||||
GH_TOKEN: "rejected-token", GIT_AUTHOR_NAME: "Rejected", GIT_AUTHOR_EMAIL: "rejected@example.test",
|
||||
GIT_COMMITTER_NAME: "Rejected", GIT_COMMITTER_EMAIL: "rejected@example.test",
|
||||
} }));
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
|
||||
cleanups.push(() => new Promise<void>(resolve => server.close(() => resolve())));
|
||||
const { port } = server.address() as { port: number };
|
||||
const env = { ...process.env, ...githubBrokerEnvironment({
|
||||
GH_TOKEN: "host-token", GIT_AUTHOR_NAME: "Host", GIT_AUTHOR_EMAIL: "host@example.test",
|
||||
GIT_COMMITTER_NAME: "Host", GIT_COMMITTER_EMAIL: "host@example.test",
|
||||
}, { url: `http://127.0.0.1:${port}`, token: access === "missing" ? "" : "run-capability" }),
|
||||
PATH: `${bin}:${process.env.PATH}` };
|
||||
await writeFile(path.join(root, "work.txt"), "saved work\n");
|
||||
await exec(path.join(bin, "git"), ["add", "work.txt"], { cwd: root, env });
|
||||
await exec(path.join(bin, "git"), ["commit", "-m", "Save local work"], { cwd: root, env });
|
||||
expect((await exec("git", ["-C", root, "log", "-1", "--format=%an <%ae>|%cn <%ce>"])).stdout.trim())
|
||||
.toBe("Paperclip <noreply@paperclip.ing>|Paperclip <noreply@paperclip.ing>");
|
||||
expect((await exec("git", ["-C", root, "show", "HEAD:work.txt"])).stdout).toBe("saved work\n");
|
||||
await expect(exec("git", ["-C", root, "config", "--local", "--get", "user.name"])).rejects.toThrow();
|
||||
await expect(exec("git", ["-C", root, "config", "--local", "--get", "user.email"])).rejects.toThrow();
|
||||
});
|
||||
it.each(["repository", "command"])("uses explicit %s identity for local commits without managed credentials", async (identitySource) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-github-local-identity-"));
|
||||
cleanups.push(() => rm(root, { recursive: true, force: true }));
|
||||
@@ -22,9 +53,8 @@ describe("managed GitHub launchers", () => {
|
||||
GH_TOKEN: "host-token", GIT_AUTHOR_NAME: "Host", GIT_COMMITTER_NAME: "Host",
|
||||
}, { url: "", token: "" }), PATH: `${bin}:${process.env.PATH}` };
|
||||
const git = async (...args: string[]) => (await exec(path.join(bin, "git"), args, { cwd: root, env })).stdout.trim();
|
||||
// No configured identity must fail, rather than guessing the host user's.
|
||||
await expect(git("var", "GIT_AUTHOR_IDENT")).rejects.toThrow();
|
||||
await expect(git("var", "GIT_COMMITTER_IDENT")).rejects.toThrow();
|
||||
expect(await git("var", "GIT_AUTHOR_IDENT")).toMatch(/^Paperclip <noreply@paperclip.ing>/);
|
||||
expect(await git("var", "GIT_COMMITTER_IDENT")).toMatch(/^Paperclip <noreply@paperclip.ing>/);
|
||||
if (identitySource === "repository") {
|
||||
await git("config", "user.name", "Local Author");
|
||||
await git("config", "user.email", "local@example.test");
|
||||
@@ -82,6 +112,44 @@ describe("managed GitHub launchers", () => {
|
||||
expect(result.stderr).toContain("More than one managed GitHub identity matches this run");
|
||||
expect(result.stderr).not.toMatch(/host-token|must-not-be-used|run-capability/);
|
||||
});
|
||||
it.each(["before-write", "partial-write"])("keeps Git and managed credentials available after a %s failure", async (failure) => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-github-default-write-failure-"));
|
||||
cleanups.push(() => rm(root, { recursive: true, force: true }));
|
||||
const bin = path.join(root, "managed"), realBin = path.join(root, "real");
|
||||
await mkdir(bin); await mkdir(realBin);
|
||||
await exec("git", ["init", root]);
|
||||
for (const name of ["git", "gh"]) await writeFile(path.join(bin, name), githubLauncherSource(), { mode: 0o700 });
|
||||
await writeFile(path.join(realBin, "gh"), '#!/usr/bin/env node\nprocess.stdout.write(process.env.GH_TOKEN || "anonymous");', { mode: 0o700 });
|
||||
const fault = path.join(root, "write-fault.cjs");
|
||||
await writeFile(fault, `const fs = require('node:fs'); const write = fs.writeFileSync;
|
||||
fs.writeFileSync = (file, ...args) => {
|
||||
if (String(file).endsWith('/gitconfig')) {
|
||||
if (${failure === "partial-write"}) write(file, '[user', { mode: 0o600 });
|
||||
throw new Error('fixture disk full');
|
||||
}
|
||||
return write(file, ...args);
|
||||
};`);
|
||||
const server = createServer((_req, res) => {
|
||||
res.setHeader("content-type", "application/json");
|
||||
res.end(JSON.stringify({ status: "available", env: { GH_TOKEN: "managed-token" } }));
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
|
||||
cleanups.push(() => new Promise<void>(resolve => server.close(() => resolve())));
|
||||
const { port } = server.address() as { port: number };
|
||||
const env = {
|
||||
...process.env, ...githubBrokerEnvironment({}, { url: `http://127.0.0.1:${port}`, token: "run-capability" }),
|
||||
PATH: `${bin}:${realBin}:${process.env.PATH}`,
|
||||
};
|
||||
const result = await exec(process.execPath, ["--require", fault, path.join(bin, "gh")], { cwd: root, env });
|
||||
expect(result.stdout).toBe("managed-token");
|
||||
expect(result.stderr).toContain("configuration_directory_unavailable");
|
||||
const git = (...args: string[]) => exec(process.execPath, ["--require", fault, path.join(bin, "git"), ...args], { cwd: root, env });
|
||||
expect((await git("status", "--porcelain", "--untracked-files=no")).stdout).toBe("");
|
||||
await expect(git("commit", "--allow-empty", "-m", "No default")).rejects.toMatchObject({ stderr: expect.stringContaining("Author identity unknown") });
|
||||
await git("-c", "user.name=Explicit", "-c", "user.email=explicit@example.test", "commit", "--allow-empty", "-m", "Explicit identity");
|
||||
expect((await exec("git", ["-C", root, "log", "-1", "--format=%an <%ae>|%cn <%ce>"])).stdout.trim())
|
||||
.toBe("Explicit <explicit@example.test>|Explicit <explicit@example.test>");
|
||||
});
|
||||
// The first broker request fails, the second succeeds: the managed token must still reach gh.
|
||||
it.each([
|
||||
["connection drops before the response", (res: import("node:http").ServerResponse) => { res.socket?.destroy(); }],
|
||||
@@ -177,7 +245,9 @@ process.stdout.write(JSON.stringify({identity, token:process.env.GH_TOKEN ?? nul
|
||||
expect(anonymous.token).toBeNull();
|
||||
await git("config", "--unset", "user.name");
|
||||
await git("config", "--unset", "user.email");
|
||||
await expect(git("var", "GIT_AUTHOR_IDENT")).rejects.toThrow();
|
||||
await git("commit", "--allow-empty", "-m", "Default identity after revocation");
|
||||
expect(await git("log", "-1", "--format=%an <%ae>|%cn <%ce>"))
|
||||
.toBe("Paperclip <noreply@paperclip.ing>|Paperclip <noreply@paperclip.ing>");
|
||||
expect(await git("status", "--porcelain")).toBe(""); // unrelated public/local Git still works
|
||||
expect(env.GH_TOKEN).toBe("");
|
||||
expect(env.GIT_AUTHOR_NAME).toBe("");
|
||||
|
||||
@@ -39,12 +39,15 @@ async function main() {
|
||||
// fallback deliberately cannot load the host's gh authentication files.
|
||||
let configDirectory = path.join(directory, 'unavailable-gh-config');
|
||||
let configReady = false;
|
||||
let authorReady = false;
|
||||
try {
|
||||
fs.mkdirSync(configRoot, { recursive: true, mode: 0o700 });
|
||||
configDirectory = fs.mkdtempSync(path.join(configRoot, 'paperclip-github-operation-'));
|
||||
fs.chmodSync(configDirectory, 0o700);
|
||||
configReady = true;
|
||||
process.once('exit', () => { try { fs.rmSync(configDirectory, { recursive: true, force: true }); } catch {} });
|
||||
fs.writeFileSync(path.join(configDirectory, 'gitconfig'), '[user]\n\tname = Paperclip\n\temail = noreply@paperclip.ing\n', { mode: 0o600 });
|
||||
authorReady = true;
|
||||
} catch { diagnostic('configuration_directory_unavailable'); }
|
||||
{
|
||||
for (const key of Object.keys(env)) {
|
||||
@@ -52,11 +55,8 @@ async function main() {
|
||||
}
|
||||
Object.assign(env, {
|
||||
GH_CONFIG_DIR: configDirectory, SSH_AUTH_SOCK: '',
|
||||
GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null',
|
||||
GIT_CONFIG_GLOBAL: authorReady ? path.join(configDirectory, 'gitconfig') : '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null',
|
||||
GIT_TERMINAL_PROMPT: '0',
|
||||
// The inherited identity was deleted above. Empty identity env values
|
||||
// override even explicit repository/command config and break local commits.
|
||||
// Require configured identity instead of guessing the OS user's details.
|
||||
GIT_CONFIG_COUNT: '5', GIT_CONFIG_KEY_0: 'credential.helper', GIT_CONFIG_VALUE_0: '',
|
||||
GIT_CONFIG_KEY_1: 'url.https://github.com/.insteadOf', GIT_CONFIG_VALUE_1: 'git@github.com:',
|
||||
GIT_CONFIG_KEY_2: 'url.https://github.com/.insteadOf', GIT_CONFIG_VALUE_2: 'ssh://git@github.com/',
|
||||
|
||||
@@ -97,7 +97,7 @@ it("keeps anonymous wrappers usable after run cleanup and excludes sandbox image
|
||||
await writeFile(path.join(root, ".config", "gh", "hosts.yml"), "ambient-image-secret");
|
||||
await writeFile(path.join(bin, "gh"), `#!${process.execPath}
|
||||
const fs = require('node:fs');
|
||||
process.stdout.write(JSON.stringify({token:process.env.GH_TOKEN || '', githubToken:process.env.GITHUB_TOKEN || '', ssh:process.env.SSH_AUTH_SOCK, global:process.env.GIT_CONFIG_GLOBAL, imageConfig:fs.existsSync(process.env.GH_CONFIG_DIR + '/hosts.yml')}));`, { mode: 0o700 });
|
||||
process.stdout.write(JSON.stringify({token:process.env.GH_TOKEN || '', githubToken:process.env.GITHUB_TOKEN || '', ssh:process.env.SSH_AUTH_SOCK, global:process.env.GIT_CONFIG_GLOBAL, globalConfig:fs.readFileSync(process.env.GIT_CONFIG_GLOBAL, 'utf8'), imageConfig:fs.existsSync(process.env.GH_CONFIG_DIR + '/hosts.yml')}));`, { mode: 0o700 });
|
||||
const imageEnv = { HOME: root, PATH: `${bin}:${path.dirname(process.execPath)}:/usr/bin:/bin`, GH_TOKEN: "ambient-token", GITHUB_TOKEN: "ambient-other-token", SSH_AUTH_SOCK: "/ambient/socket" };
|
||||
const execute: CommandManagedRuntimeRunner["execute"] = async (input) => {
|
||||
const startedAt = new Date().toISOString();
|
||||
@@ -117,7 +117,14 @@ process.stdout.write(JSON.stringify({token:process.env.GH_TOKEN || '', githubTok
|
||||
expect(await readFile(path.join(first.env.PAPERCLIP_GITHUB_LAUNCHER_DIR, "gh"), "utf8")).not.toContain("ambient-token");
|
||||
// A still-live provider uses its first-turn environment, not the new one.
|
||||
const command = await execute({ command: path.join(first.env.PAPERCLIP_GITHUB_LAUNCHER_DIR, "gh"), env: first.env });
|
||||
expect(JSON.parse(command.stdout)).toEqual({ token: "", githubToken: "", ssh: "", global: "/dev/null", imageConfig: false });
|
||||
const result = JSON.parse(command.stdout);
|
||||
expect(result).toEqual({ token: "", githubToken: "", ssh: "", imageConfig: false,
|
||||
global: expect.stringMatching(/\/paperclip-github-operation-[^/]+\/gitconfig$/),
|
||||
globalConfig: "[user]\n\tname = Paperclip\n\temail = noreply@paperclip.ing\n" });
|
||||
await expect(readFile(result.global)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
const commit = await execute({ command: "bash", args: ["--noprofile", "--norc", "-c",
|
||||
"git init -q && git commit --allow-empty -qm 'Local work' && git log -1 --format='%an <%ae>|%cn <%ce>'"], env: first.env });
|
||||
expect(commit.stdout.trim()).toBe("Paperclip <noreply@paperclip.ing>|Paperclip <noreply@paperclip.ing>");
|
||||
const changedPath = await prepareHeartbeatGitHubLaunchers({ ...base, runId: "run-three", env: { ...imageEnv, PATH: `${imageEnv.PATH}:/extra` } });
|
||||
expect(changedPath.env.PAPERCLIP_GITHUB_LAUNCHER_DIR).not.toBe(first.env.PAPERCLIP_GITHUB_LAUNCHER_DIR);
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user