@sagentlab/navarch-runtime 0.1.136 → 0.1.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/git-credential-navarch.cjs +31 -9
- package/dist/git-worktree.cjs +41 -5
- package/dist/github-credential.cjs +99 -1
- package/dist/session.cjs +63 -16
- package/dist/worktree-guard.cjs +24 -1
- package/package.json +1 -1
|
@@ -4,19 +4,43 @@
|
|
|
4
4
|
// for every remote operation, so each lookup gets a fresh installation token
|
|
5
5
|
// from the lease-scoped broker instead of relying on the session-start token.
|
|
6
6
|
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
|
|
7
9
|
const operation = process.argv[2];
|
|
8
10
|
if (operation !== "get") process.exit(0);
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
// Sandboxed sessions (Codex guard) cannot reach the Navarch API, so the
|
|
13
|
+
// runtime parent refreshes the token into this file and the helper only
|
|
14
|
+
// reads it.
|
|
15
|
+
const tokenFile = process.env.NAVARCH_GIT_CREDENTIAL_TOKEN_FILE;
|
|
16
|
+
if (tokenFile) {
|
|
17
|
+
let token;
|
|
18
|
+
try {
|
|
19
|
+
token = fs.readFileSync(tokenFile, "utf8").trim();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`navarch git credential is unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
if (!token || /[\r\n\0]/.test(token)) {
|
|
25
|
+
console.error("navarch git credential file is empty or malformed");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
// No process.exit here: on macOS a piped stdout write is asynchronous.
|
|
29
|
+
process.stdout.write(`username=x-access-token\npassword=${token}\n`);
|
|
30
|
+
} else {
|
|
31
|
+
void main();
|
|
17
32
|
}
|
|
18
33
|
|
|
19
34
|
async function main() {
|
|
35
|
+
const apiBase = process.env.NAVARCH_GIT_CREDENTIAL_API_BASE?.replace(/\/+$/, "");
|
|
36
|
+
const machineToken = process.env.NAVARCH_GIT_CREDENTIAL_MACHINE_TOKEN;
|
|
37
|
+
const leaseId = process.env.NAVARCH_GIT_CREDENTIAL_LEASE_ID;
|
|
38
|
+
|
|
39
|
+
if (!apiBase || !machineToken || !leaseId) {
|
|
40
|
+
console.error("navarch git credential helper is missing broker configuration");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
try {
|
|
21
45
|
const response = await fetch(`${apiBase}/api/broker/issue`, {
|
|
22
46
|
method: "POST",
|
|
@@ -42,5 +66,3 @@ async function main() {
|
|
|
42
66
|
process.exitCode = 1;
|
|
43
67
|
}
|
|
44
68
|
}
|
|
45
|
-
|
|
46
|
-
void main();
|
package/dist/git-worktree.cjs
CHANGED
|
@@ -48,6 +48,8 @@ class GitWorktree {
|
|
|
48
48
|
checkpointRoot;
|
|
49
49
|
cloneUrl;
|
|
50
50
|
githubToken;
|
|
51
|
+
githubTokenProvider;
|
|
52
|
+
repoLocalTokenInstalled = false;
|
|
51
53
|
gitGrant;
|
|
52
54
|
workingBranch;
|
|
53
55
|
repoLocalGithubToken;
|
|
@@ -92,6 +94,7 @@ class GitWorktree {
|
|
|
92
94
|
? proxyCloneUrl(options.cloneUrl, options.gitGrant.proxyBaseUrl)
|
|
93
95
|
: options.cloneUrl;
|
|
94
96
|
this.githubToken = options.gitGrant ? undefined : options.githubToken;
|
|
97
|
+
this.githubTokenProvider = options.gitGrant ? undefined : options.githubTokenProvider;
|
|
95
98
|
this.workingBranch = options.workingBranch?.trim() || null;
|
|
96
99
|
this.repoLocalGithubToken = options.repoLocalGithubToken;
|
|
97
100
|
this.prContinuation = options.prContinuation ?? false;
|
|
@@ -135,13 +138,44 @@ class GitWorktree {
|
|
|
135
138
|
async installRepoLocalGithubToken() {
|
|
136
139
|
if (!this.repoLocalGithubToken)
|
|
137
140
|
return;
|
|
138
|
-
|
|
141
|
+
const token = this.githubTokenProvider?.() ?? this.repoLocalGithubToken;
|
|
142
|
+
if (/[\0\r\n]/.test(token)) {
|
|
139
143
|
throw new Error("Broker-issued GitHub credential contains unsupported control characters.");
|
|
140
144
|
}
|
|
141
145
|
const configPath = node_path_1.default.join(this.repositoryPath, "config");
|
|
142
|
-
const block = repoLocalGithubTokenBlock(this.sessionTokenMarker,
|
|
146
|
+
const block = repoLocalGithubTokenBlock(this.sessionTokenMarker, token);
|
|
143
147
|
await node_fs_1.promises.chmod(configPath, 0o600);
|
|
144
148
|
await node_fs_1.promises.appendFile(configPath, block, { encoding: "utf8", mode: 0o600 });
|
|
149
|
+
this.repoLocalTokenInstalled = true;
|
|
150
|
+
}
|
|
151
|
+
/** Serializes rotation with prepare/cleanup; never recreates a removed repository. */
|
|
152
|
+
async refreshRepoLocalGithubToken(token) {
|
|
153
|
+
if (!this.repoLocalGithubToken)
|
|
154
|
+
return;
|
|
155
|
+
if (/[\0\r\n]/.test(token))
|
|
156
|
+
throw new Error("Invalid refreshed GitHub credential");
|
|
157
|
+
await withRepositoryLock(this.repositoryPath, async () => {
|
|
158
|
+
if (!this.repoLocalTokenInstalled)
|
|
159
|
+
return;
|
|
160
|
+
const configPath = node_path_1.default.join(this.repositoryPath, "config");
|
|
161
|
+
// Cooperate with git config writers in the agent as well as parent operations.
|
|
162
|
+
const lockPath = `${configPath}.lock`;
|
|
163
|
+
const lock = await node_fs_1.promises.open(lockPath, "wx", 0o600);
|
|
164
|
+
let renamed = false;
|
|
165
|
+
try {
|
|
166
|
+
const config = await node_fs_1.promises.readFile(configPath, "utf8");
|
|
167
|
+
const clean = removeMarkedConfigBlock(config, this.sessionTokenMarker);
|
|
168
|
+
await lock.writeFile(clean + repoLocalGithubTokenBlock(this.sessionTokenMarker, token), "utf8");
|
|
169
|
+
await lock.close();
|
|
170
|
+
await node_fs_1.promises.rename(lockPath, configPath);
|
|
171
|
+
renamed = true;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await lock.close().catch(() => undefined);
|
|
175
|
+
if (!renamed)
|
|
176
|
+
await node_fs_1.promises.rm(lockPath, { force: true });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
145
179
|
}
|
|
146
180
|
/**
|
|
147
181
|
* Installs the delivery-branch guard shared by every worktree of this cache.
|
|
@@ -688,6 +722,7 @@ class GitWorktree {
|
|
|
688
722
|
async removeRepoLocalGithubToken() {
|
|
689
723
|
if (!this.repoLocalGithubToken)
|
|
690
724
|
return;
|
|
725
|
+
this.repoLocalTokenInstalled = false;
|
|
691
726
|
const configPath = node_path_1.default.join(this.repositoryPath, "config");
|
|
692
727
|
try {
|
|
693
728
|
const config = await node_fs_1.promises.readFile(configPath, "utf8");
|
|
@@ -710,7 +745,8 @@ class GitWorktree {
|
|
|
710
745
|
}
|
|
711
746
|
return grantResult;
|
|
712
747
|
}
|
|
713
|
-
const
|
|
748
|
+
const githubToken = this.githubTokenProvider ? this.githubTokenProvider() : this.githubToken;
|
|
749
|
+
const credentialArgs = authenticated && githubToken
|
|
714
750
|
? [
|
|
715
751
|
"-c",
|
|
716
752
|
"credential.helper=",
|
|
@@ -719,8 +755,8 @@ class GitWorktree {
|
|
|
719
755
|
]
|
|
720
756
|
: [];
|
|
721
757
|
const result = await this.runner.run("git", [...credentialArgs, ...args], {
|
|
722
|
-
env:
|
|
723
|
-
? { ...process.env, GITHUB_TOKEN:
|
|
758
|
+
env: githubToken
|
|
759
|
+
? { ...process.env, GITHUB_TOKEN: githubToken, GIT_TERMINAL_PROMPT: "0" }
|
|
724
760
|
: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
725
761
|
});
|
|
726
762
|
if (result.code !== 0) {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GITHUB_CREDENTIAL_SECRET_NAME = void 0;
|
|
6
|
+
exports.GithubTokenFileRefresher = exports.GITHUB_TOKEN_FILENAME = exports.GITHUB_TOKEN_FILE_RETRY_MS = exports.GITHUB_TOKEN_FILE_REFRESH_MS = exports.GITHUB_CREDENTIAL_SECRET_NAME = void 0;
|
|
4
7
|
exports.githubCredentialDenialSummary = githubCredentialDenialSummary;
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const git_grant_cjs_1 = require("./git-grant.cjs");
|
|
5
11
|
/** Wire name the session requests its repository credential under. */
|
|
6
12
|
exports.GITHUB_CREDENTIAL_SECRET_NAME = "github-pat";
|
|
7
13
|
/**
|
|
@@ -41,3 +47,95 @@ function githubCredentialDenialSummary(denied) {
|
|
|
41
47
|
return (`GitHub credential denied (${denial.reason}): ${explanation} ` +
|
|
42
48
|
"The session stopped before cloning instead of falling back to this machine's own git login.");
|
|
43
49
|
}
|
|
50
|
+
/** GitHub App installation tokens live an hour; refresh well inside that. */
|
|
51
|
+
exports.GITHUB_TOKEN_FILE_REFRESH_MS = 10 * 60 * 1000;
|
|
52
|
+
exports.GITHUB_TOKEN_FILE_RETRY_MS = 60 * 1000;
|
|
53
|
+
exports.GITHUB_TOKEN_FILENAME = "github-token";
|
|
54
|
+
/**
|
|
55
|
+
* Keeps managed repository credentials current for git, gh, and parent operations.
|
|
56
|
+
* The runtime parent fetches from the broker, so the machine token never enters
|
|
57
|
+
* the agent environment. Helpers and gh only read session-owned files, including
|
|
58
|
+
* under the Codex worktree guard. Mirrors GitGrantRefresher.
|
|
59
|
+
*/
|
|
60
|
+
class GithubTokenFileRefresher {
|
|
61
|
+
options;
|
|
62
|
+
token;
|
|
63
|
+
get current() { return this.token; }
|
|
64
|
+
timer = null;
|
|
65
|
+
stopped = false;
|
|
66
|
+
inflight = null;
|
|
67
|
+
constructor(options) {
|
|
68
|
+
this.options = options;
|
|
69
|
+
}
|
|
70
|
+
/** Writes the session-start token and schedules broker refreshes. */
|
|
71
|
+
async start(initialToken) {
|
|
72
|
+
await this.write(initialToken);
|
|
73
|
+
this.schedule(this.options.refreshMs ?? exports.GITHUB_TOKEN_FILE_REFRESH_MS);
|
|
74
|
+
}
|
|
75
|
+
async refresh() {
|
|
76
|
+
const issued = await this.options.api.issueSecrets({
|
|
77
|
+
lease_id: this.options.leaseId,
|
|
78
|
+
secret_names: [exports.GITHUB_CREDENTIAL_SECRET_NAME],
|
|
79
|
+
});
|
|
80
|
+
const token = issued.secrets[exports.GITHUB_CREDENTIAL_SECRET_NAME];
|
|
81
|
+
if (typeof token !== "string" || token.length === 0)
|
|
82
|
+
throw new Error("broker did not issue github-pat");
|
|
83
|
+
if (this.stopped)
|
|
84
|
+
throw new Error("github token refresher stopped");
|
|
85
|
+
await this.write(token);
|
|
86
|
+
}
|
|
87
|
+
/** Stops, waits for an in-flight refresh, then removes the token file. */
|
|
88
|
+
async stopAndClean() {
|
|
89
|
+
this.stopped = true;
|
|
90
|
+
if (this.timer)
|
|
91
|
+
clearTimeout(this.timer);
|
|
92
|
+
this.timer = null;
|
|
93
|
+
await this.inflight?.catch(() => undefined);
|
|
94
|
+
await promises_1.default.rm(this.options.tokenFile, { force: true }).catch(() => undefined);
|
|
95
|
+
if (this.options.ghConfigDir) {
|
|
96
|
+
await promises_1.default.rm(node_path_1.default.join(this.options.ghConfigDir, "hosts.yml"), { force: true }).catch(() => undefined);
|
|
97
|
+
}
|
|
98
|
+
this.token = undefined;
|
|
99
|
+
}
|
|
100
|
+
async write(token) {
|
|
101
|
+
// Register before writing so a redaction pass never misses a live value.
|
|
102
|
+
this.options.registry.register(token);
|
|
103
|
+
if (/[\r\n\0]/.test(token))
|
|
104
|
+
throw new Error("github token contains unsupported control characters");
|
|
105
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(this.options.tokenFile), { recursive: true, mode: 0o700 });
|
|
106
|
+
await (0, git_grant_cjs_1.writePrivateFile)(this.options.tokenFile, `${token}\n`);
|
|
107
|
+
if (this.options.ghConfigDir) {
|
|
108
|
+
await promises_1.default.mkdir(this.options.ghConfigDir, { recursive: true, mode: 0o700 });
|
|
109
|
+
// gh otherwise tries to migrate (and rewrite) hosts.yml inside the read-only sandbox.
|
|
110
|
+
await promises_1.default.writeFile(node_path_1.default.join(this.options.ghConfigDir, "config.yml"), "version: 1\n", { mode: 0o600, flag: "wx" })
|
|
111
|
+
.catch((error) => { if (error.code !== "EEXIST")
|
|
112
|
+
throw error; });
|
|
113
|
+
await (0, git_grant_cjs_1.writePrivateFile)(node_path_1.default.join(this.options.ghConfigDir, "hosts.yml"), (0, git_grant_cjs_1.renderGhHostsYml)("github.com", token));
|
|
114
|
+
}
|
|
115
|
+
this.token = token;
|
|
116
|
+
await this.options.onToken?.(token);
|
|
117
|
+
}
|
|
118
|
+
schedule(delayMs) {
|
|
119
|
+
if (this.stopped)
|
|
120
|
+
return;
|
|
121
|
+
this.timer = setTimeout(() => {
|
|
122
|
+
this.inflight = this.tick();
|
|
123
|
+
}, delayMs);
|
|
124
|
+
this.timer.unref?.();
|
|
125
|
+
}
|
|
126
|
+
async tick() {
|
|
127
|
+
if (this.stopped)
|
|
128
|
+
return;
|
|
129
|
+
try {
|
|
130
|
+
await this.refresh();
|
|
131
|
+
this.schedule(this.options.refreshMs ?? exports.GITHUB_TOKEN_FILE_REFRESH_MS);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// The previous token stays until its own expiry; GitHub rejects it after.
|
|
135
|
+
const retryMs = this.options.retryMs ?? exports.GITHUB_TOKEN_FILE_RETRY_MS;
|
|
136
|
+
this.options.log.warn(`github token refresh failed for ${this.options.leaseId}; retrying in ${retryMs / 1000}s: ${String(error)}`);
|
|
137
|
+
this.schedule(retryMs);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.GithubTokenFileRefresher = GithubTokenFileRefresher;
|
package/dist/session.cjs
CHANGED
|
@@ -45,6 +45,7 @@ const commit_attribution_cjs_1 = require("./commit-attribution.cjs");
|
|
|
45
45
|
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
46
46
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
47
47
|
const GIT_CREDENTIAL_HELPER_FILENAME = "git-credential-navarch.cjs";
|
|
48
|
+
const SESSION_GIT_GLOBAL_CONFIG_FILENAME = "gitconfig";
|
|
48
49
|
const GIT_GRANT_HELPER_FILENAME = "git-credential-navarch-grant.cjs";
|
|
49
50
|
const COMPLETION_REMEDIATION_RETRIES = 2;
|
|
50
51
|
/**
|
|
@@ -105,13 +106,19 @@ function resolveSessionExecution(config, claimed) {
|
|
|
105
106
|
* isolation instead; see runtime/README.md.
|
|
106
107
|
*/
|
|
107
108
|
async function runSession(deps, claimed, sessionId) {
|
|
108
|
-
const lifecycle = { enteredMainSessionTry: false };
|
|
109
|
+
const lifecycle = { enteredMainSessionTry: false, refreshers: [] };
|
|
109
110
|
try {
|
|
110
111
|
return await runClaimedSession(deps, claimed, sessionId, lifecycle);
|
|
111
112
|
}
|
|
112
113
|
catch (err) {
|
|
113
114
|
if (lifecycle.enteredMainSessionTry)
|
|
114
115
|
throw err;
|
|
116
|
+
// Stop credential refreshers first: they would otherwise keep calling the
|
|
117
|
+
// broker for the lease completed below and recreate the removed files.
|
|
118
|
+
// stopAndClean waits for an in-flight refresh and is idempotent.
|
|
119
|
+
for (const refresher of lifecycle.refreshers) {
|
|
120
|
+
await refresher.stopAndClean().catch((stopErr) => log.warn(`credential refresher cleanup failed after initialization crash: ${String(stopErr)}`));
|
|
121
|
+
}
|
|
115
122
|
// Initialization before the main worktree/adapter try (prompt creation,
|
|
116
123
|
// broker access, MCP/guard setup) must still release the lease with the
|
|
117
124
|
// explicit pre-start signal. Otherwise it waits for expiry and bypasses
|
|
@@ -185,6 +192,13 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
185
192
|
// The runtime parent fetches and refreshes the grant; the agent never sees
|
|
186
193
|
// the machine token or a GitHub token in these modes.
|
|
187
194
|
const bundleGitMode = bundle.repository?.git_proxy_mode;
|
|
195
|
+
// Codex's worktree guard is an OS sandbox around the whole agent, git's
|
|
196
|
+
// credential helper included: it denies the session dir and allows only
|
|
197
|
+
// GitHub hosts. Helpers and their token files go in a dir it may read.
|
|
198
|
+
const codexGuarded = config.sandboxMode === "host" && config.worktreeGuard && runtime === "codex";
|
|
199
|
+
const gitCredentialDir = (0, worktree_guard_cjs_1.sessionGitCredentialDir)(workDir);
|
|
200
|
+
let githubTokenRefresher;
|
|
201
|
+
const credentialConsumer = {};
|
|
188
202
|
let gitGrantRefresher;
|
|
189
203
|
let gitGrantEnv;
|
|
190
204
|
if (bundleGitMode !== "off") {
|
|
@@ -200,9 +214,11 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
200
214
|
});
|
|
201
215
|
try {
|
|
202
216
|
const issue = await refresher.start();
|
|
203
|
-
|
|
217
|
+
await node_fs_1.promises.mkdir(gitCredentialDir, { recursive: true, mode: 0o700 });
|
|
218
|
+
const helperPath = node_path_1.default.join(gitCredentialDir, GIT_GRANT_HELPER_FILENAME);
|
|
204
219
|
await node_fs_1.promises.copyFile(node_path_1.default.resolve(__dirname, "../bin/git-credential-navarch-grant.cjs"), helperPath);
|
|
205
220
|
gitGrantRefresher = refresher;
|
|
221
|
+
lifecycle.refreshers.push(refresher);
|
|
206
222
|
gitGrantEnv = {
|
|
207
223
|
proxyHost: issue.proxy_host,
|
|
208
224
|
proxyBaseUrl: issue.proxy_base_url,
|
|
@@ -256,14 +272,18 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
256
272
|
}
|
|
257
273
|
let gitCredentialRefresh;
|
|
258
274
|
if (!gitGrantMode && managedGithubCredential && secrets["github-pat"]) {
|
|
259
|
-
|
|
275
|
+
await node_fs_1.promises.mkdir(gitCredentialDir, { recursive: true, mode: 0o700 });
|
|
276
|
+
const helperPath = node_path_1.default.join(gitCredentialDir, GIT_CREDENTIAL_HELPER_FILENAME);
|
|
260
277
|
await node_fs_1.promises.copyFile(node_path_1.default.resolve(__dirname, "../bin/git-credential-navarch.cjs"), helperPath);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
};
|
|
278
|
+
const tokenFile = node_path_1.default.join(gitCredentialDir, github_credential_cjs_1.GITHUB_TOKEN_FILENAME);
|
|
279
|
+
githubTokenRefresher = new github_credential_cjs_1.GithubTokenFileRefresher({
|
|
280
|
+
api, leaseId, tokenFile, registry, log,
|
|
281
|
+
ghConfigDir: (0, worktree_guard_cjs_1.sessionGhConfigDir)(workDir),
|
|
282
|
+
onToken: async (token) => { await credentialConsumer.worktree?.refreshRepoLocalGithubToken(token); },
|
|
283
|
+
});
|
|
284
|
+
lifecycle.refreshers.push(githubTokenRefresher);
|
|
285
|
+
await githubTokenRefresher.start(secrets["github-pat"]);
|
|
286
|
+
gitCredentialRefresh = { helperPath, tokenFile };
|
|
267
287
|
}
|
|
268
288
|
const sessionEnv = toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail, gitGrantEnv);
|
|
269
289
|
// Proxied projects (CX-3.7): the control-plane identity replaces the
|
|
@@ -274,6 +294,15 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
274
294
|
const ghConfigDir = (0, worktree_guard_cjs_1.sessionGhConfigDir)(workDir);
|
|
275
295
|
await node_fs_1.promises.mkdir(ghConfigDir, { recursive: true, mode: 0o700 });
|
|
276
296
|
sessionEnv.GH_CONFIG_DIR = ghConfigDir;
|
|
297
|
+
if (codexGuarded) {
|
|
298
|
+
// The sandbox cannot read ~/.gitconfig, and git exits on an unreadable
|
|
299
|
+
// global config, so every agent git command failed until the agent found
|
|
300
|
+
// GIT_CONFIG_GLOBAL=/dev/null itself. The session file also replaces the
|
|
301
|
+
// default ~/.config/git/ignore, whose denial warns on every status.
|
|
302
|
+
await node_fs_1.promises.mkdir(gitCredentialDir, { recursive: true, mode: 0o700 });
|
|
303
|
+
sessionEnv.GIT_CONFIG_GLOBAL = node_path_1.default.join(gitCredentialDir, SESSION_GIT_GLOBAL_CONFIG_FILENAME);
|
|
304
|
+
await node_fs_1.promises.writeFile(sessionEnv.GIT_CONFIG_GLOBAL, "[core]\n\texcludesFile = /dev/null\n", { mode: 0o600 });
|
|
305
|
+
}
|
|
277
306
|
if (gitGrantMode) {
|
|
278
307
|
// Adapters spread process.env under this map. Blank any GitHub token the
|
|
279
308
|
// runtime process itself holds so the agent cannot inherit it.
|
|
@@ -297,6 +326,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
297
326
|
});
|
|
298
327
|
secrets = {};
|
|
299
328
|
await gitGrantRefresher?.stopAndClean();
|
|
329
|
+
await githubTokenRefresher?.stopAndClean();
|
|
300
330
|
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
301
331
|
return;
|
|
302
332
|
}
|
|
@@ -316,6 +346,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
316
346
|
sessionId,
|
|
317
347
|
cloneUrl,
|
|
318
348
|
githubToken,
|
|
349
|
+
githubTokenProvider: githubTokenRefresher ? () => githubTokenRefresher.current : undefined,
|
|
319
350
|
workingBranch: bundle.repository?.working_branch,
|
|
320
351
|
// Grant modes never expose a GitHub token, so the Codex repo-local
|
|
321
352
|
// github-pat bootstrap is not installed.
|
|
@@ -330,6 +361,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
330
361
|
restoreHostedCheckpoint: !allowBranchRename && task.task_type !== "verify",
|
|
331
362
|
...(commitIdentity ? { ownershipCommitIdentity: commitIdentity.committer } : {}),
|
|
332
363
|
});
|
|
364
|
+
credentialConsumer.worktree = gitWorktree;
|
|
333
365
|
const knownGuidanceIds = new Set((bundle.guidance ?? []).map((entry) => entry.id));
|
|
334
366
|
const deliveredGuidance = [...(bundle.guidance ?? [])];
|
|
335
367
|
let guidanceCursor = bundle.guidance_cursor ?? null;
|
|
@@ -730,6 +762,11 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
730
762
|
workspaceRoot: config.workspaceRoot,
|
|
731
763
|
extraRoots: config.guardExtraRoots,
|
|
732
764
|
codexBin: config.codexBin,
|
|
765
|
+
credentialReadRoots: [
|
|
766
|
+
gitCredentialDir,
|
|
767
|
+
...(gitGrantEnv ? [(0, git_grant_cjs_1.gitGrantPaths)(workDir).dir] : []),
|
|
768
|
+
],
|
|
769
|
+
networkHosts: gitGrantEnv ? [gitGrantEnv.proxyHost] : [],
|
|
733
770
|
});
|
|
734
771
|
}
|
|
735
772
|
const restoredCheckpoint = gitWorktree.restoredHostedCheckpoint;
|
|
@@ -1127,7 +1164,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
1127
1164
|
// Keep SHA verification on the exact branch, but do not adopt a
|
|
1128
1165
|
// foreign-branch PR as delivery evidence for a root task.
|
|
1129
1166
|
allowShaFallback: allowBranchRename,
|
|
1130
|
-
githubToken: gitGrantMode ? gitGrantRefresher?.current?.grant : githubToken,
|
|
1167
|
+
githubToken: gitGrantMode ? gitGrantRefresher?.current?.grant : githubTokenRefresher?.current ?? githubToken,
|
|
1131
1168
|
// Grant modes read through the proxy and never use the host's own gh login.
|
|
1132
1169
|
apiBaseUrl: gitGrantEnv ? `${gitGrantEnv.proxyBaseUrl.replace(/\/+$/, "")}/api/v3` : undefined,
|
|
1133
1170
|
hostTokenProvider: gitGrantMode ? async () => undefined : undefined,
|
|
@@ -1424,9 +1461,10 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
1424
1461
|
// request. A later heartbeat response sees sessionClosing and does nothing.
|
|
1425
1462
|
await Promise.resolve(heartbeatWorkspaceUpdate).catch(() => undefined);
|
|
1426
1463
|
secrets = {};
|
|
1427
|
-
// Waits for an in-flight refresh, then removes the grant
|
|
1428
|
-
// survives in a retained workspace.
|
|
1464
|
+
// Waits for an in-flight refresh, then removes the grant and token files
|
|
1465
|
+
// so none survives in a retained workspace.
|
|
1429
1466
|
await gitGrantRefresher?.stopAndClean();
|
|
1467
|
+
await githubTokenRefresher?.stopAndClean();
|
|
1430
1468
|
// A sandbox teardown failure must not skip worktree cleanup: a leftover
|
|
1431
1469
|
// task branch in the shared cache blocks the task's next retry.
|
|
1432
1470
|
try {
|
|
@@ -1665,12 +1703,21 @@ function toEnvMap(secrets, credentialRefreshOrAuthorName, gitAuthorNameOrEmail =
|
|
|
1665
1703
|
["user.name", gitAuthorName],
|
|
1666
1704
|
["user.email", resolvedGitAuthorEmail],
|
|
1667
1705
|
];
|
|
1668
|
-
if (secrets["github-pat"] && !out.GITHUB_TOKEN) {
|
|
1706
|
+
if (secrets["github-pat"] && (!out.GITHUB_TOKEN || (gitCredentialRefresh && "tokenFile" in gitCredentialRefresh))) {
|
|
1669
1707
|
out.GITHUB_TOKEN = secrets["github-pat"];
|
|
1670
1708
|
if (gitCredentialRefresh) {
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1709
|
+
if ("tokenFile" in gitCredentialRefresh) {
|
|
1710
|
+
out.NAVARCH_GIT_CREDENTIAL_TOKEN_FILE = gitCredentialRefresh.tokenFile;
|
|
1711
|
+
// gh must read the rotating hosts.yml, including when a parent has GH_TOKEN.
|
|
1712
|
+
out.GITHUB_TOKEN = "";
|
|
1713
|
+
out.GH_TOKEN = "";
|
|
1714
|
+
out.GH_HOST = "github.com";
|
|
1715
|
+
}
|
|
1716
|
+
else {
|
|
1717
|
+
out.NAVARCH_GIT_CREDENTIAL_API_BASE = gitCredentialRefresh.apiBase;
|
|
1718
|
+
out.NAVARCH_GIT_CREDENTIAL_MACHINE_TOKEN = gitCredentialRefresh.machineToken;
|
|
1719
|
+
out.NAVARCH_GIT_CREDENTIAL_LEASE_ID = gitCredentialRefresh.leaseId;
|
|
1720
|
+
}
|
|
1674
1721
|
gitConfig.unshift(["credential.helper", ""], ["credential.helper", `!node ${shellQuote(gitCredentialRefresh.helperPath)}`]);
|
|
1675
1722
|
out.GIT_TERMINAL_PROMPT = "0";
|
|
1676
1723
|
}
|
package/dist/worktree-guard.cjs
CHANGED
|
@@ -11,6 +11,7 @@ exports.codexExecutableReadRoots = codexExecutableReadRoots;
|
|
|
11
11
|
exports.codexHelperReadRoots = codexHelperReadRoots;
|
|
12
12
|
exports.macosDeveloperWriteRoots = macosDeveloperWriteRoots;
|
|
13
13
|
exports.sessionGhConfigDir = sessionGhConfigDir;
|
|
14
|
+
exports.sessionGitCredentialDir = sessionGitCredentialDir;
|
|
14
15
|
exports.sessionCodexTmpDir = sessionCodexTmpDir;
|
|
15
16
|
exports.codexWorktreeGuardArgs = codexWorktreeGuardArgs;
|
|
16
17
|
exports.geminiSandboxMounts = geminiSandboxMounts;
|
|
@@ -19,6 +20,7 @@ const node_fs_1 = require("node:fs");
|
|
|
19
20
|
const node_os_1 = __importDefault(require("node:os"));
|
|
20
21
|
const CODEX_GUARD_PROFILE = "navarch-worktree";
|
|
21
22
|
const SESSION_GH_CONFIG_DIRNAME = "gh-config";
|
|
23
|
+
const SESSION_GIT_CREDENTIAL_DIRNAME = "git-credential";
|
|
22
24
|
const CODEX_GITHUB_NETWORK_DOMAINS = {
|
|
23
25
|
"**.github.com": "allow",
|
|
24
26
|
"**.githubusercontent.com": "allow",
|
|
@@ -378,6 +380,14 @@ function macosDeveloperWriteRoots(env = process.env, platform = process.platform
|
|
|
378
380
|
function sessionGhConfigDir(workDir) {
|
|
379
381
|
return node_path_1.default.join(workDir, SESSION_GH_CONFIG_DIRNAME);
|
|
380
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Session-owned directory for git's credential helpers and the token file
|
|
385
|
+
* they read. It sits beside the session metadata, which the Codex profile
|
|
386
|
+
* denies, so it is granted read-only by name (credentialReadRoots).
|
|
387
|
+
*/
|
|
388
|
+
function sessionGitCredentialDir(workDir) {
|
|
389
|
+
return node_path_1.default.join(workDir, SESSION_GIT_CREDENTIAL_DIRNAME);
|
|
390
|
+
}
|
|
381
391
|
function sessionCodexTmpDir(workDir) {
|
|
382
392
|
return node_path_1.default.resolve(workDir, "tmp");
|
|
383
393
|
}
|
|
@@ -417,6 +427,16 @@ function codexWorktreeGuardArgs(options, platform = process.platform) {
|
|
|
417
427
|
[node_path_1.default.resolve(options.repositoryPath)]: "write",
|
|
418
428
|
[sessionGhConfigDir(options.workDir)]: "read",
|
|
419
429
|
};
|
|
430
|
+
// git runs the credential helper inside the sandbox. Without these grants
|
|
431
|
+
// the helper copied into the denied session dir looks missing, and every
|
|
432
|
+
// authenticated fetch or push fails (ShiftSwap, 2026-10-27).
|
|
433
|
+
// Only strict subdirectories of this session: never the metadata dir itself.
|
|
434
|
+
const sessionDir = node_path_1.default.resolve(options.workDir);
|
|
435
|
+
for (const root of options.credentialReadRoots ?? []) {
|
|
436
|
+
const resolved = node_path_1.default.resolve(root);
|
|
437
|
+
if (resolved !== sessionDir && isPathInside(resolved, sessionDir))
|
|
438
|
+
filesystem[resolved] = "read";
|
|
439
|
+
}
|
|
420
440
|
// Codex protects a linked worktree's gitdir with a more specific read-only
|
|
421
441
|
// rule. A write grant on the bare repository alone does not override it:
|
|
422
442
|
// fetch/checkout/commit fail on FETCH_HEAD, HEAD.lock, or index.lock. Grant
|
|
@@ -471,7 +491,10 @@ function codexWorktreeGuardArgs(options, platform = process.platform) {
|
|
|
471
491
|
"-c",
|
|
472
492
|
`permissions.${CODEX_GUARD_PROFILE}.network.enabled=true`,
|
|
473
493
|
"-c",
|
|
474
|
-
`permissions.${CODEX_GUARD_PROFILE}.network.domains=${tomlInlineTable(
|
|
494
|
+
`permissions.${CODEX_GUARD_PROFILE}.network.domains=${tomlInlineTable({
|
|
495
|
+
...CODEX_GITHUB_NETWORK_DOMAINS,
|
|
496
|
+
...Object.fromEntries((options.networkHosts ?? []).map((host) => [host, "allow"])),
|
|
497
|
+
})}`,
|
|
475
498
|
];
|
|
476
499
|
}
|
|
477
500
|
function linkedWorktreeGitDir(worktreePath, repositoryPath) {
|
package/package.json
CHANGED