@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.
@@ -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. The cache avoids N full clones while
15
- * git's worktree metadata keeps concurrent agents from sharing an index or
16
- * working directory.
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 = node_path_1.default.join(options.workspaceRoot, "repositories", `${projectKey}.git`);
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, held only in memory (registry + env map below),
75
- // never written to disk on the host. They only ever reach disk inside the
76
- // sandbox's tmpfs (sandbox.cts injectEnv), which is wiped with the container.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through Claude Code, Codex, or Gemini CLI.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",