@sagentlab/navarch-runtime 0.1.21 → 0.1.22
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/dist/git-worktree.cjs +74 -4
- package/dist/session.cjs +6 -3
- package/package.json +1 -1
package/dist/git-worktree.cjs
CHANGED
|
@@ -11,9 +11,9 @@ const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
|
11
11
|
const repositoryLocks = new Map();
|
|
12
12
|
/**
|
|
13
13
|
* Maintains one bare repository cache per project and checks out each session
|
|
14
|
-
* into its own uniquely named worktree.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* into its own uniquely named worktree. Sessions that need a repo-local secret
|
|
15
|
+
* use an isolated bare repository under their session root because linked
|
|
16
|
+
* worktrees share their repository-local config.
|
|
17
17
|
*/
|
|
18
18
|
class GitWorktree {
|
|
19
19
|
sessionRoot;
|
|
@@ -23,16 +23,20 @@ class GitWorktree {
|
|
|
23
23
|
runner;
|
|
24
24
|
cloneUrl;
|
|
25
25
|
githubToken;
|
|
26
|
+
repoLocalGithubToken;
|
|
26
27
|
taskBranchSuffix;
|
|
27
28
|
legacyTaskBranchSuffix;
|
|
28
29
|
taskOwnershipTrailer;
|
|
30
|
+
sessionTokenMarker;
|
|
29
31
|
constructor(options) {
|
|
30
32
|
const projectKey = safePathSegment(options.projectId);
|
|
31
33
|
const sessionKey = safePathSegment(options.sessionId);
|
|
32
34
|
const taskKey = safePathSegment(options.taskId);
|
|
33
35
|
this.sessionRoot = node_path_1.default.join(options.workspaceRoot, "sessions", sessionKey);
|
|
34
36
|
this.worktreePath = node_path_1.default.join(this.sessionRoot, "repo");
|
|
35
|
-
this.repositoryPath =
|
|
37
|
+
this.repositoryPath = options.repoLocalGithubToken
|
|
38
|
+
? node_path_1.default.join(this.sessionRoot, "repository.git")
|
|
39
|
+
: node_path_1.default.join(options.workspaceRoot, "repositories", `${projectKey}.git`);
|
|
36
40
|
// One delivery task owns one remote branch across every retry. A
|
|
37
41
|
// session-scoped branch lets a failed attempt push useful work, then makes
|
|
38
42
|
// the retry start from main and open a second PR for the same task.
|
|
@@ -40,10 +44,12 @@ class GitWorktree {
|
|
|
40
44
|
this.taskBranchSuffix = `-${normalizedTaskId.slice(0, 8)}`;
|
|
41
45
|
this.legacyTaskBranchSuffix = `-${normalizedTaskId}`;
|
|
42
46
|
this.taskOwnershipTrailer = `Navarch-Task-ID: ${normalizedTaskId}`;
|
|
47
|
+
this.sessionTokenMarker = `navarch-session-github-token:${sessionKey}`;
|
|
43
48
|
this.branch = `navarch/${branchSlug(options)}${this.taskBranchSuffix}`;
|
|
44
49
|
this.runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
|
|
45
50
|
this.cloneUrl = options.cloneUrl;
|
|
46
51
|
this.githubToken = options.githubToken;
|
|
52
|
+
this.repoLocalGithubToken = options.repoLocalGithubToken;
|
|
47
53
|
}
|
|
48
54
|
async prepare() {
|
|
49
55
|
await node_fs_1.promises.mkdir(node_path_1.default.dirname(this.repositoryPath), { recursive: true });
|
|
@@ -63,8 +69,31 @@ class GitWorktree {
|
|
|
63
69
|
`could not resolve the fetched start ref. The shared cache was preserved to avoid invalidating ` +
|
|
64
70
|
`active worktrees; retry after active sessions finish or repair the cache in place: ${errorMessage(error)}`);
|
|
65
71
|
}
|
|
72
|
+
await this.installRepoLocalGithubToken();
|
|
66
73
|
});
|
|
67
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Makes a broker-issued GitHub credential visible to workflows that require
|
|
77
|
+
* `git config --local --get codex.githubToken`.
|
|
78
|
+
*
|
|
79
|
+
* Git ignores command-scope, worktree-scope, and included values when
|
|
80
|
+
* `--local` is requested, so the compatibility key has to live in the bare
|
|
81
|
+
* repository config. Token-bearing sessions use an isolated repository to
|
|
82
|
+
* keep concurrent worktrees from observing the value. The marked block lets
|
|
83
|
+
* cleanup remove exactly this session's value. The secret is written via fs
|
|
84
|
+
* and never passed in argv or emitted by the command runner.
|
|
85
|
+
*/
|
|
86
|
+
async installRepoLocalGithubToken() {
|
|
87
|
+
if (!this.repoLocalGithubToken)
|
|
88
|
+
return;
|
|
89
|
+
if (/[\0\r]/.test(this.repoLocalGithubToken)) {
|
|
90
|
+
throw new Error("Broker-issued GitHub credential contains unsupported control characters.");
|
|
91
|
+
}
|
|
92
|
+
const configPath = node_path_1.default.join(this.repositoryPath, "config");
|
|
93
|
+
const block = repoLocalGithubTokenBlock(this.sessionTokenMarker, this.repoLocalGithubToken);
|
|
94
|
+
await node_fs_1.promises.chmod(configPath, 0o600);
|
|
95
|
+
await node_fs_1.promises.appendFile(configPath, block, { encoding: "utf8", mode: 0o600 });
|
|
96
|
+
}
|
|
68
97
|
/** Clones the bare cache if missing, repoints origin if needed, and fetches all branches. */
|
|
69
98
|
async ensureRepositoryCache() {
|
|
70
99
|
if (!(await pathExists(node_path_1.default.join(this.repositoryPath, "HEAD")))) {
|
|
@@ -231,6 +260,7 @@ class GitWorktree {
|
|
|
231
260
|
}
|
|
232
261
|
async cleanup() {
|
|
233
262
|
await withRepositoryLock(this.repositoryPath, async () => {
|
|
263
|
+
await this.removeRepoLocalGithubToken();
|
|
234
264
|
await this.runner
|
|
235
265
|
.run("git", ["--git-dir", this.repositoryPath, "worktree", "remove", "--force", this.worktreePath])
|
|
236
266
|
.catch(() => undefined);
|
|
@@ -242,6 +272,21 @@ class GitWorktree {
|
|
|
242
272
|
.catch(() => undefined);
|
|
243
273
|
});
|
|
244
274
|
}
|
|
275
|
+
async removeRepoLocalGithubToken() {
|
|
276
|
+
if (!this.repoLocalGithubToken)
|
|
277
|
+
return;
|
|
278
|
+
const configPath = node_path_1.default.join(this.repositoryPath, "config");
|
|
279
|
+
try {
|
|
280
|
+
const config = await node_fs_1.promises.readFile(configPath, "utf8");
|
|
281
|
+
const cleaned = removeMarkedConfigBlock(config, this.sessionTokenMarker);
|
|
282
|
+
if (cleaned !== config)
|
|
283
|
+
await node_fs_1.promises.writeFile(configPath, cleaned, { mode: 0o600 });
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (error.code !== "ENOENT")
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
245
290
|
async runGit(args, authenticated) {
|
|
246
291
|
const credentialArgs = authenticated && this.githubToken
|
|
247
292
|
? [
|
|
@@ -263,6 +308,31 @@ class GitWorktree {
|
|
|
263
308
|
}
|
|
264
309
|
}
|
|
265
310
|
exports.GitWorktree = GitWorktree;
|
|
311
|
+
function repoLocalGithubTokenBlock(marker, token) {
|
|
312
|
+
const escaped = token
|
|
313
|
+
.replace(/\\/g, "\\\\")
|
|
314
|
+
.replace(/"/g, '\\"')
|
|
315
|
+
.replace(/\n/g, "\\n")
|
|
316
|
+
.replace(/\t/g, "\\t")
|
|
317
|
+
.replace(/\u0008/g, "\\b");
|
|
318
|
+
return (`\n# ${marker}:begin\n` +
|
|
319
|
+
`[codex]\n` +
|
|
320
|
+
`\tgithubToken = "${escaped}"\n` +
|
|
321
|
+
`# ${marker}:end\n`);
|
|
322
|
+
}
|
|
323
|
+
function removeMarkedConfigBlock(config, marker) {
|
|
324
|
+
const begin = `# ${marker}:begin`;
|
|
325
|
+
const end = `# ${marker}:end`;
|
|
326
|
+
const start = config.indexOf(begin);
|
|
327
|
+
if (start < 0)
|
|
328
|
+
return config;
|
|
329
|
+
const blockStart = start > 0 && config[start - 1] === "\n" ? start - 1 : start;
|
|
330
|
+
const endStart = config.indexOf(end, start + begin.length);
|
|
331
|
+
if (endStart < 0)
|
|
332
|
+
return config;
|
|
333
|
+
const endNewline = config.indexOf("\n", endStart + end.length);
|
|
334
|
+
return config.slice(0, blockStart) + config.slice(endNewline < 0 ? config.length : endNewline + 1);
|
|
335
|
+
}
|
|
266
336
|
async function withRepositoryLock(key, work) {
|
|
267
337
|
const previous = repositoryLocks.get(key) ?? Promise.resolve();
|
|
268
338
|
let release;
|
package/dist/session.cjs
CHANGED
|
@@ -71,9 +71,11 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
71
71
|
await (0, worktree_janitor_cjs_1.markSessionWorkspaceActive)(workDir);
|
|
72
72
|
const promptText = (0, prompt_cjs_1.renderPrompt)(task, bundle);
|
|
73
73
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
|
|
74
|
-
// Secrets: initially fetched once
|
|
75
|
-
//
|
|
76
|
-
//
|
|
74
|
+
// Secrets: initially fetched once and held in the registry + child env map.
|
|
75
|
+
// The broker-issued github-pat is additionally installed in the project-local
|
|
76
|
+
// git config after worktree creation for the mandated Codex gh bootstrap;
|
|
77
|
+
// GitWorktree.cleanup removes that session-marked entry before teardown.
|
|
78
|
+
// Docker env injection remains tmpfs-backed (sandbox.cts injectEnv).
|
|
77
79
|
const registry = new redact_cjs_1.SecretRegistry();
|
|
78
80
|
let secrets = {};
|
|
79
81
|
let managedGithubCredential = false;
|
|
@@ -127,6 +129,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
127
129
|
sessionId,
|
|
128
130
|
cloneUrl,
|
|
129
131
|
githubToken,
|
|
132
|
+
repoLocalGithubToken: secrets["github-pat"],
|
|
130
133
|
});
|
|
131
134
|
const knownGuidanceIds = new Set((bundle.guidance ?? []).map((entry) => entry.id));
|
|
132
135
|
const deliveredGuidance = [...(bundle.guidance ?? [])];
|
package/package.json
CHANGED