@sagentlab/navarch-runtime 0.1.8 → 0.1.9

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/README.md CHANGED
@@ -60,8 +60,9 @@ project only (it will never be dispatched work from any other project).
60
60
 
61
61
  ```sh
62
62
  npx @sagentlab/navarch-runtime connect --token flmt_<...> --project <project-id> \
63
- --name my-agent-1 --agent codex --api-base https://navarch.example.com
64
- npx @sagentlab/navarch-runtime supervise
63
+ --name my-agent-1 --agent codex --api-base https://navarch.example.com \
64
+ --config-dir ~/.navarch/my-agent-1
65
+ npx @sagentlab/navarch-runtime supervise --config-dir ~/.navarch/my-agent-1
65
66
  ```
66
67
 
67
68
  (`@sagentlab/navarch-runtime` is published to npm, so `npx
@@ -79,6 +80,30 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
79
80
  | `supervise [--agent claude-code\|codex]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
80
81
  | `doctor` | Prints resolved config + Docker/registration status; no side effects. |
81
82
 
83
+ ### Running multiple agents on one machine
84
+
85
+ Every agent instance must have its own config directory. The directory contains
86
+ the machine identity, update state, managed runtime versions, and (by default)
87
+ session workspaces. Reusing it lets a later `connect` replace `machine.json` and
88
+ causes both processes to race over update and workspace state.
89
+
90
+ Pass the same `--config-dir` to `connect` and `supervise` for each instance:
91
+
92
+ ```sh
93
+ npx @sagentlab/navarch-runtime connect <pebble enrollment options> \
94
+ --name pebble-agent-2816 --agent codex --config-dir ~/.navarch/pebble
95
+ npx @sagentlab/navarch-runtime supervise --config-dir ~/.navarch/pebble
96
+
97
+ npx @sagentlab/navarch-runtime connect <tobi enrollment options> \
98
+ --name tobi-agent-1521 --agent codex --config-dir ~/.navarch/tobi
99
+ npx @sagentlab/navarch-runtime supervise --config-dir ~/.navarch/tobi
100
+ ```
101
+
102
+ `NAVARCH_CONFIG_DIR` remains equivalent for service managers and environment
103
+ files. A supervisor also pins its starting identity into replacement workers,
104
+ so an accidental later edit to `machine.json` cannot change that running
105
+ instance during an automatic-update handoff.
106
+
82
107
  ## Active-task guidance
83
108
 
84
109
  Guidance added to a task that is already running is delivered on that
@@ -183,7 +208,7 @@ unchanged across the deployment.
183
208
  | `NAVARCH_MACHINE_NAME` | — | Used by `register`. |
184
209
  | `NAVARCH_ENROLLMENT_TOKEN` | — | Alternative to `register --token` / `connect --token`. |
185
210
  | `NAVARCH_PROJECT_ID` | — | Alternative to `connect --project`. |
186
- | `NAVARCH_CONFIG_DIR` | `~/.navarch` | Where `machine.json` lives. |
211
+ | `NAVARCH_CONFIG_DIR` | `~/.navarch` | Per-instance state root. Equivalent to `--config-dir`; use a different directory for every agent on the same host. |
187
212
  | `NAVARCH_WORKSPACE_ROOT` | `<config dir>/sandboxes` | Persistent bare repo caches plus isolated per-session worktrees. |
188
213
  | `NAVARCH_MAX_SESSIONS` | `5` | Local concurrent-session capacity cap — see `src/capacity.cts`. |
189
214
  | `NAVARCH_CAPABILITIES` | `shell` (`docker-sandbox,shell` in Docker mode) | Comma list reported at heartbeat/claim time. |
@@ -192,6 +217,7 @@ unchanged across the deployment.
192
217
  | `NAVARCH_HEARTBEAT_INTERVAL_MS` | `60000` | Machine-level heartbeat interval. |
193
218
  | `NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS` | `300000` | Per-lease heartbeat interval; must stay well under the 15-minute lease TTL (schema-design.md §4). |
194
219
  | `NAVARCH_SESSION_TIMEOUT_MS` | `2700000` (45 min) | Hard kill timeout for a single session. |
220
+ | `NAVARCH_GIT_AUTHOR_NAME` / `NAVARCH_GIT_AUTHOR_EMAIL` | `sagentlab` / `z@sagentlab.com` | Git identity forced into session commits so host-level personal config is not inherited; override both for a project-authorized bot. |
195
221
  | `NAVARCH_SANDBOX_MODE` | `host` | `host` uses the resources already available to the agent process. Set `docker` explicitly for container isolation. |
196
222
  | `NAVARCH_DOCKER_IMAGE` | `node:20-slim` | Image used for the per-session container. |
197
223
  | `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent CLI: `claude-code` or `codex`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Git credential helper for managed Navarch repositories. Git invokes this
4
+ // for every remote operation, so each lookup gets a fresh installation token
5
+ // from the lease-scoped broker instead of relying on the session-start token.
6
+
7
+ const operation = process.argv[2];
8
+ if (operation !== "get") process.exit(0);
9
+
10
+ const apiBase = process.env.NAVARCH_GIT_CREDENTIAL_API_BASE?.replace(/\/+$/, "");
11
+ const machineToken = process.env.NAVARCH_GIT_CREDENTIAL_MACHINE_TOKEN;
12
+ const leaseId = process.env.NAVARCH_GIT_CREDENTIAL_LEASE_ID;
13
+
14
+ if (!apiBase || !machineToken || !leaseId) {
15
+ console.error("navarch git credential helper is missing broker configuration");
16
+ process.exit(1);
17
+ }
18
+
19
+ async function main() {
20
+ try {
21
+ const response = await fetch(`${apiBase}/api/broker/issue`, {
22
+ method: "POST",
23
+ headers: {
24
+ authorization: `Bearer ${machineToken}`,
25
+ "content-type": "application/json",
26
+ },
27
+ body: JSON.stringify({ lease_id: leaseId, secret_names: ["github-pat"] }),
28
+ signal: AbortSignal.timeout(15_000),
29
+ });
30
+ if (!response.ok) {
31
+ throw new Error(`broker returned HTTP ${response.status}`);
32
+ }
33
+ const result = await response.json();
34
+ const token = result?.secrets?.["github-pat"];
35
+ if (typeof token !== "string" || token.length === 0) {
36
+ throw new Error("broker did not issue github-pat");
37
+ }
38
+ process.stdout.write(`username=x-access-token\npassword=${token}\n`);
39
+ } catch (error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ console.error(`navarch git credential refresh failed: ${message}`);
42
+ process.exitCode = 1;
43
+ }
44
+ }
45
+
46
+ void main();
package/dist/cli.cjs CHANGED
@@ -1,8 +1,12 @@
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
6
  exports.main = main;
4
7
  const config_cjs_1 = require("./config.cjs");
5
8
  const node_crypto_1 = require("node:crypto");
9
+ const node_path_1 = __importDefault(require("node:path"));
6
10
  const machine_store_cjs_1 = require("./machine-store.cjs");
7
11
  const api_cjs_1 = require("./api.cjs");
8
12
  const capacity_cjs_1 = require("./capacity.cjs");
@@ -32,6 +36,17 @@ function invocation(subcommand) {
32
36
  const prefix = viaNpx ? `npx ${PACKAGE_NAME}` : "navarch-runtime";
33
37
  return `${prefix} ${subcommand}`;
34
38
  }
39
+ /** Keep the post-enrollment next step pinned to the identity we just saved. */
40
+ function superviseInvocation(configDir) {
41
+ return `${invocation("supervise")} --config-dir ${configDir}`;
42
+ }
43
+ /** Resolve CLI-local configuration without mutating the parent shell environment. */
44
+ function configFromFlags(flags) {
45
+ const configDir = flags["config-dir"];
46
+ return (0, config_cjs_1.loadRuntimeConfig)(configDir
47
+ ? { ...process.env, NAVARCH_CONFIG_DIR: node_path_1.default.resolve(configDir) }
48
+ : process.env);
49
+ }
35
50
  function agentFromFlag(flags) {
36
51
  const value = flags.agent;
37
52
  if (value === undefined)
@@ -66,7 +81,7 @@ function parseArgs(argv) {
66
81
  * RegisterMachineRequest for the (assumed, to-confirm) endpoint contract.
67
82
  */
68
83
  async function registerCommand(flags) {
69
- const config = (0, config_cjs_1.loadRuntimeConfig)();
84
+ const config = configFromFlags(flags);
70
85
  const apiBase = flags["api-base"] ?? config.apiBase;
71
86
  const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
72
87
  const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
@@ -102,7 +117,7 @@ async function registerCommand(flags) {
102
117
  console.log("Machine registered.");
103
118
  console.log(` machine_id: ${result.machine_id}`);
104
119
  console.log(` token: ${result.token}`);
105
- console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("supervise")}\` to begin serving tasks.`);
120
+ console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${superviseInvocation(config.configDir)}\` to begin serving tasks.`);
106
121
  }
107
122
  /**
108
123
  * `navarch-runtime connect` — "Connect an agent to a project"
@@ -114,7 +129,7 @@ async function registerCommand(flags) {
114
129
  * machine token exactly once, same discipline as `register`.
115
130
  */
116
131
  async function connectCommand(flags) {
117
- const config = (0, config_cjs_1.loadRuntimeConfig)();
132
+ const config = configFromFlags(flags);
118
133
  const apiBase = flags["api-base"] ?? config.apiBase;
119
134
  const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
120
135
  const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
@@ -150,10 +165,10 @@ async function connectCommand(flags) {
150
165
  console.log("Machine connected.");
151
166
  console.log(` machine_id: ${result.machine_id}`);
152
167
  console.log(` token: ${result.token}`);
153
- console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("supervise")}\` to begin serving tasks.`);
168
+ console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${superviseInvocation(config.configDir)}\` to begin serving tasks.`);
154
169
  }
155
170
  async function startCommand(flags) {
156
- const baseConfig = (0, config_cjs_1.loadRuntimeConfig)();
171
+ const baseConfig = configFromFlags(flags);
157
172
  const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(baseConfig.configDir, baseConfig.apiBase);
158
173
  // Adapter selection belongs to the local machine: an explicit start flag
159
174
  // wins, followed by NAVARCH_AGENT, the choice saved at connect/register
@@ -203,14 +218,25 @@ async function startCommand(flags) {
203
218
  process.on("SIGTERM", shutdown);
204
219
  }
205
220
  async function superviseCommand(flags) {
206
- const config = (0, config_cjs_1.loadRuntimeConfig)();
221
+ const config = configFromFlags(flags);
222
+ const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(config.configDir, config.apiBase);
207
223
  const agentType = agentFromFlag(flags);
208
224
  const workerArgs = agentType ? ["--agent", agentType] : [];
209
- const exitCode = await (0, supervisor_cjs_1.superviseRuntime)(config.configDir, process.argv[1] ?? "", workerArgs);
225
+ // Pin the identity for this supervisor's lifetime. Without this snapshot, a
226
+ // replacement worker rereads machine.json after an automatic update and can
227
+ // silently become a different machine if another terminal reused the same
228
+ // config directory in the meantime.
229
+ const exitCode = await (0, supervisor_cjs_1.superviseRuntime)(config.configDir, process.argv[1] ?? "", workerArgs, 120_000, {
230
+ NAVARCH_CONFIG_DIR: config.configDir,
231
+ NAVARCH_MACHINE_ID: identity.machine_id,
232
+ NAVARCH_MACHINE_TOKEN: identity.token,
233
+ NAVARCH_MACHINE_NAME: identity.name,
234
+ NAVARCH_API_BASE: identity.api_base,
235
+ });
210
236
  process.exitCode = exitCode;
211
237
  }
212
238
  async function doctorCommand(flags) {
213
- const config = (0, config_cjs_1.loadRuntimeConfig)();
239
+ const config = configFromFlags(flags);
214
240
  const dockerOk = await (0, sandbox_cjs_1.isDockerAvailable)();
215
241
  console.log(`api_base: ${config.apiBase}`);
216
242
  console.log(`config_dir: ${config.configDir}`);
@@ -237,14 +263,15 @@ function helpText() {
237
263
 
238
264
  Usage:
239
265
  navarch-runtime register --token <enrollment-token> --name <machine-name> \\
240
- [--agent claude-code|codex] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
266
+ [--config-dir <path>] [--agent claude-code|codex] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
241
267
  navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
242
- [--agent claude-code|codex] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
243
- navarch-runtime start [--agent claude-code|codex]
244
- navarch-runtime supervise [--agent claude-code|codex]
245
- navarch-runtime doctor
268
+ [--config-dir <path>] [--agent claude-code|codex] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
269
+ navarch-runtime start [--config-dir <path>] [--agent claude-code|codex]
270
+ navarch-runtime supervise [--config-dir <path>] [--agent claude-code|codex]
271
+ navarch-runtime doctor [--config-dir <path>]
246
272
 
247
- Configuration is via NAVARCH_* environment variables; see runtime/README.md.
273
+ Use a different --config-dir (or NAVARCH_CONFIG_DIR) for every agent instance.
274
+ Other configuration is via NAVARCH_* environment variables; see runtime/README.md.
248
275
  `;
249
276
  }
250
277
  async function main(argv = process.argv.slice(2)) {
package/dist/config.cjs CHANGED
@@ -57,6 +57,8 @@ function loadRuntimeConfig(env = process.env) {
57
57
  claudeExtraArgs: envList(env, "NAVARCH_CLAUDE_EXTRA_ARGS", []),
58
58
  codexBin: env.NAVARCH_CODEX_BIN ?? "codex",
59
59
  codexExtraArgs: envList(env, "NAVARCH_CODEX_EXTRA_ARGS", []),
60
+ gitAuthorName: env.NAVARCH_GIT_AUTHOR_NAME ?? "sagentlab",
61
+ gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
60
62
  mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
61
63
  sandboxMode,
62
64
  dockerImage: env.NAVARCH_DOCKER_IMAGE ?? "node:20-slim",
@@ -38,40 +38,95 @@ class GitWorktree {
38
38
  await node_fs_1.promises.mkdir(node_path_1.default.dirname(this.repositoryPath), { recursive: true });
39
39
  await node_fs_1.promises.mkdir(this.sessionRoot, { recursive: true });
40
40
  await withRepositoryLock(this.repositoryPath, async () => {
41
- if (!(await pathExists(node_path_1.default.join(this.repositoryPath, "HEAD")))) {
42
- await this.runGit(["clone", "--bare", this.cloneUrl, this.repositoryPath], true);
41
+ await this.ensureRepositoryCache();
42
+ try {
43
+ await this.addSessionWorktree();
43
44
  }
44
- else {
45
- const origin = await this.runGit(["--git-dir", this.repositoryPath, "remote", "get-url", "origin"], false);
46
- if (normalizeCloneUrl(origin.stdout) !== normalizeCloneUrl(this.cloneUrl)) {
47
- await this.runGit(["--git-dir", this.repositoryPath, "remote", "set-url", "origin", this.cloneUrl], false);
48
- }
45
+ catch (error) {
46
+ if (!isInvalidReferenceError(error))
47
+ throw error;
48
+ // This bare cache owns the metadata for every active session worktree.
49
+ // Replacing it here would invalidate those worktrees, so leave it intact
50
+ // and fail with enough context for a coordinated repair.
51
+ throw new Error(`Failed to create a session worktree because the cached repository at ${this.repositoryPath} ` +
52
+ `could not resolve the fetched start ref. The shared cache was preserved to avoid invalidating ` +
53
+ `active worktrees; retry after active sessions finish or repair the cache in place: ${errorMessage(error)}`);
49
54
  }
50
- const remoteHead = await this.runGit(["--git-dir", this.repositoryPath, "ls-remote", "--symref", "origin", "HEAD"], true);
51
- const defaultBranch = parseRemoteHead(remoteHead.stdout);
52
- const startRef = defaultBranch
53
- ? defaultBranch.replace(/^refs\/heads\//, "refs/remotes/origin/")
54
- : "HEAD";
55
- await this.runGit([
56
- "--git-dir",
57
- this.repositoryPath,
58
- "fetch",
59
- "--prune",
60
- "origin",
61
- "+refs/heads/*:refs/remotes/origin/*",
62
- ], true);
63
- await this.runGit([
64
- "--git-dir",
65
- this.repositoryPath,
66
- "worktree",
67
- "add",
68
- "-b",
69
- this.branch,
70
- this.worktreePath,
71
- startRef,
72
- ], false);
73
55
  });
74
56
  }
57
+ /** Clones the bare cache if missing, repoints origin if needed, and fetches all branches. */
58
+ async ensureRepositoryCache() {
59
+ if (!(await pathExists(node_path_1.default.join(this.repositoryPath, "HEAD")))) {
60
+ await this.runGit(["clone", "--bare", this.cloneUrl, this.repositoryPath], true);
61
+ }
62
+ else {
63
+ const origin = await this.runGit(["--git-dir", this.repositoryPath, "remote", "get-url", "origin"], false);
64
+ if (normalizeCloneUrl(origin.stdout) !== normalizeCloneUrl(this.cloneUrl)) {
65
+ await this.runGit(["--git-dir", this.repositoryPath, "remote", "set-url", "origin", this.cloneUrl], false);
66
+ }
67
+ }
68
+ await this.runGit([
69
+ "--git-dir",
70
+ this.repositoryPath,
71
+ "fetch",
72
+ "--prune",
73
+ "origin",
74
+ "+refs/heads/*:refs/remotes/origin/*",
75
+ ], true);
76
+ }
77
+ async addSessionWorktree() {
78
+ const startRef = await this.resolveStartRef();
79
+ if (!startRef) {
80
+ // Empty remote (no commits yet): there is nothing to base the session on,
81
+ // so bootstrap an orphan branch the session can push as the first commit.
82
+ await this.addOrphanWorktree();
83
+ return;
84
+ }
85
+ await this.runGit([
86
+ "--git-dir",
87
+ this.repositoryPath,
88
+ "worktree",
89
+ "add",
90
+ "-b",
91
+ this.branch,
92
+ this.worktreePath,
93
+ startRef,
94
+ ], false);
95
+ }
96
+ /**
97
+ * Resolves the remote-tracking ref new session branches start from, or null
98
+ * when the remote has no branches at all (a freshly provisioned empty repo).
99
+ */
100
+ async resolveStartRef() {
101
+ const remoteHead = await this.runGit(["--git-dir", this.repositoryPath, "ls-remote", "--symref", "origin", "HEAD"], true);
102
+ const defaultBranch = parseRemoteHead(remoteHead.stdout);
103
+ if (defaultBranch)
104
+ return defaultBranch.replace(/^refs\/heads\//, "refs/remotes/origin/");
105
+ // No symref (e.g. an unborn remote HEAD): fall back to whatever branches
106
+ // the fetch brought in, preferring the conventional default names.
107
+ const refs = await this.runGit(["--git-dir", this.repositoryPath, "for-each-ref", "--format=%(refname)", "refs/remotes/origin"], false);
108
+ const branches = refs.stdout
109
+ .split("\n")
110
+ .map((line) => line.trim())
111
+ .filter((line) => line && line !== "refs/remotes/origin/HEAD");
112
+ for (const preferred of ["refs/remotes/origin/main", "refs/remotes/origin/master"]) {
113
+ if (branches.includes(preferred))
114
+ return preferred;
115
+ }
116
+ return branches[0] ?? null;
117
+ }
118
+ async addOrphanWorktree() {
119
+ try {
120
+ await this.runGit(["--git-dir", this.repositoryPath, "worktree", "add", "--orphan", "-b", this.branch, this.worktreePath], false);
121
+ }
122
+ catch (error) {
123
+ // `worktree add --orphan` needs git >= 2.42; older gits reject the flag.
124
+ if (!/unknown option|usage: git worktree add/i.test(errorMessage(error)))
125
+ throw error;
126
+ await this.runGit(["init", "--initial-branch", this.branch, this.worktreePath], false);
127
+ await this.runGit(["-C", this.worktreePath, "remote", "add", "origin", this.cloneUrl], false);
128
+ }
129
+ }
75
130
  async cleanup() {
76
131
  await withRepositoryLock(this.repositoryPath, async () => {
77
132
  await this.runner
@@ -172,3 +227,9 @@ function parseRemoteHead(output) {
172
227
  const match = output.match(/^ref:\s+(refs\/heads\/[A-Za-z0-9._/-]+)\s+HEAD$/m);
173
228
  return match?.[1] ?? null;
174
229
  }
230
+ function errorMessage(error) {
231
+ return error instanceof Error ? error.message : String(error);
232
+ }
233
+ function isInvalidReferenceError(error) {
234
+ return /invalid reference/i.test(errorMessage(error));
235
+ }
package/dist/session.cjs CHANGED
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runSession = runSession;
7
+ exports.toEnvMap = toEnvMap;
7
8
  const node_path_1 = __importDefault(require("node:path"));
8
9
  const node_fs_1 = require("node:fs");
9
10
  const sandbox_cjs_1 = require("./sandbox.cjs");
@@ -19,11 +20,13 @@ const github_pr_cjs_1 = require("./github-pr.cjs");
19
20
  const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
20
21
  /** Filename the generated platform MCP config is written under inside the session metadata directory. */
21
22
  const MCP_CONFIG_FILENAME = "mcp-config.json";
23
+ const GIT_CREDENTIAL_HELPER_FILENAME = "git-credential-navarch.cjs";
22
24
  const log = (0, logger_cjs_1.createLogger)("session");
23
25
  /**
24
26
  * Runs one claimed task end to end (implementation-plan.md WP-07):
25
27
  * 1. write the prompt file
26
- * 2. fetch secrets from the broker once, at session start
28
+ * 2. fetch secrets from the broker at session start (managed GitHub git
29
+ * credentials are subsequently refreshed by a lease-scoped helper)
27
30
  * 3. optionally stand up a Docker sandbox when explicitly configured
28
31
  * 4. run the configured agent adapter (Claude Code or Codex, per
29
32
  * NAVARCH_AGENT — adapters/index.cts#selectAdapter), heartbeating the
@@ -58,17 +61,30 @@ async function runSession(deps, claimed, sessionId) {
58
61
  await node_fs_1.promises.mkdir(workDir, { recursive: true });
59
62
  const promptText = (0, prompt_cjs_1.renderPrompt)(task, bundle);
60
63
  await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
61
- // Secrets: fetched once, held only in memory (registry + env map below),
64
+ // Secrets: initially fetched once, held only in memory (registry + env map below),
62
65
  // never written to disk on the host. They only ever reach disk inside the
63
66
  // sandbox's tmpfs (sandbox.cts injectEnv), which is wiped with the container.
64
67
  const registry = new redact_cjs_1.SecretRegistry();
65
68
  let secrets = {};
69
+ let managedGithubCredential = false;
66
70
  const secretNames = bundle.secret_manifest.map((s) => s.name);
67
71
  if (secretNames.length > 0) {
68
72
  const issued = await api.issueSecrets({ lease_id: leaseId, secret_names: secretNames });
69
73
  secrets = issued.secrets;
74
+ managedGithubCredential = issued.github_app_issued === true;
70
75
  registry.registerAll(secrets);
71
76
  }
77
+ let gitCredentialRefresh;
78
+ if (managedGithubCredential && secrets["github-pat"]) {
79
+ const helperPath = node_path_1.default.join(workDir, GIT_CREDENTIAL_HELPER_FILENAME);
80
+ await node_fs_1.promises.copyFile(node_path_1.default.resolve(__dirname, "../bin/git-credential-navarch.cjs"), helperPath);
81
+ gitCredentialRefresh = {
82
+ helperPath,
83
+ apiBase: api.getBaseUrl(),
84
+ machineToken: api.getToken() ?? "",
85
+ leaseId,
86
+ };
87
+ }
72
88
  const cloneUrl = bundle.repository?.clone_url ??
73
89
  (task.repo ? `https://github.com/${task.repo.replace(/\.git$/, "")}.git` : null);
74
90
  if (!cloneUrl) {
@@ -223,7 +239,7 @@ async function runSession(deps, claimed, sessionId) {
223
239
  await gitWorktree.prepare();
224
240
  if (sandbox) {
225
241
  await sandbox.create();
226
- await sandbox.injectEnv(toEnvMap(secrets));
242
+ await sandbox.injectEnv(toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail));
227
243
  }
228
244
  // Picks the Claude Code or Codex adapter per NAVARCH_AGENT
229
245
  // (config.cts's `agentType`) — see adapters/index.cts#selectAdapter.
@@ -255,7 +271,7 @@ async function runSession(deps, claimed, sessionId) {
255
271
  model: execution.model,
256
272
  reasoningEffort: execution.reasoning_effort,
257
273
  timeoutMs: config.sessionTimeoutMs,
258
- env: toEnvMap(secrets),
274
+ env: toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail),
259
275
  settingsPath: claudeSettingsPath,
260
276
  codexGuardArgs,
261
277
  cwd: sandbox ? undefined : gitWorktree.worktreePath,
@@ -358,21 +374,53 @@ async function runSession(deps, claimed, sessionId) {
358
374
  }
359
375
  }
360
376
  /** Uppercases + sanitizes secret names into shell-safe env var names for injectEnv(). */
361
- function toEnvMap(secrets) {
377
+ function toEnvMap(secrets, credentialRefreshOrAuthorName, gitAuthorNameOrEmail = "sagentlab", gitAuthorEmail = "z@sagentlab.com") {
378
+ // Keep the existing `(secrets, authorName, authorEmail)` call shape while
379
+ // accepting a refresh config before the author identity for managed repos.
380
+ const gitCredentialRefresh = typeof credentialRefreshOrAuthorName === "string"
381
+ ? undefined
382
+ : credentialRefreshOrAuthorName;
383
+ const gitAuthorName = typeof credentialRefreshOrAuthorName === "string"
384
+ ? credentialRefreshOrAuthorName
385
+ : gitAuthorNameOrEmail;
386
+ const resolvedGitAuthorEmail = typeof credentialRefreshOrAuthorName === "string"
387
+ ? gitAuthorNameOrEmail
388
+ : gitAuthorEmail;
362
389
  const out = {};
363
390
  for (const [name, value] of Object.entries(secrets)) {
364
391
  out[name.toUpperCase().replace(/[^A-Z0-9_]/g, "_")] = value;
365
392
  }
393
+ const gitConfig = [
394
+ ["user.name", gitAuthorName],
395
+ ["user.email", resolvedGitAuthorEmail],
396
+ ];
366
397
  if (secrets["github-pat"] && !out.GITHUB_TOKEN) {
367
398
  out.GITHUB_TOKEN = secrets["github-pat"];
368
- // Make ordinary `git push` calls from the agent use the in-memory token.
369
- // The helper contains only an env-var reference; the token itself never
370
- // lands in argv, git config, or the worktree.
371
- out.GIT_CONFIG_COUNT = "1";
372
- out.GIT_CONFIG_KEY_0 = "credential.helper";
373
- out.GIT_CONFIG_VALUE_0 =
374
- '!f() { echo username=x-access-token; echo "password=$GITHUB_TOKEN"; }; f';
375
- out.GIT_TERMINAL_PROMPT = "0";
399
+ if (gitCredentialRefresh) {
400
+ out.NAVARCH_GIT_CREDENTIAL_API_BASE = gitCredentialRefresh.apiBase;
401
+ out.NAVARCH_GIT_CREDENTIAL_MACHINE_TOKEN = gitCredentialRefresh.machineToken;
402
+ out.NAVARCH_GIT_CREDENTIAL_LEASE_ID = gitCredentialRefresh.leaseId;
403
+ gitConfig.unshift(["credential.helper", ""], ["credential.helper", `!node ${shellQuote(gitCredentialRefresh.helperPath)}`]);
404
+ out.GIT_TERMINAL_PROMPT = "0";
405
+ }
406
+ else {
407
+ // Make ordinary `git push` calls from the agent use the in-memory token.
408
+ // The helper contains only an env-var reference; the token itself never
409
+ // lands in argv, git config, or the worktree.
410
+ gitConfig.unshift([
411
+ "credential.helper",
412
+ '!f() { echo username=x-access-token; echo "password=$GITHUB_TOKEN"; }; f',
413
+ ]);
414
+ out.GIT_TERMINAL_PROMPT = "0";
415
+ }
376
416
  }
417
+ out.GIT_CONFIG_COUNT = String(gitConfig.length);
418
+ gitConfig.forEach(([key, value], index) => {
419
+ out[`GIT_CONFIG_KEY_${index}`] = key;
420
+ out[`GIT_CONFIG_VALUE_${index}`] = value;
421
+ });
377
422
  return out;
378
423
  }
424
+ function shellQuote(value) {
425
+ return `'${value.replace(/'/g, `'\\''`)}'`;
426
+ }
@@ -6,10 +6,10 @@ const logger_cjs_1 = require("./logger.cjs");
6
6
  const update_installer_cjs_1 = require("./update-installer.cjs");
7
7
  const log = (0, logger_cjs_1.createLogger)("supervisor");
8
8
  const UPDATE_RESTART_EXIT_CODE = 75;
9
- function runWorker(binPath, args, healthTimeoutMs, setCurrentChild, onHealthy) {
9
+ function runWorker(binPath, args, healthTimeoutMs, workerEnv, setCurrentChild, onHealthy) {
10
10
  return new Promise((resolve) => {
11
11
  const child = (0, node_child_process_1.spawn)(process.execPath, [binPath, "start", ...args], {
12
- env: { ...process.env, NAVARCH_SUPERVISED: "1" },
12
+ env: { ...process.env, ...workerEnv, NAVARCH_SUPERVISED: "1" },
13
13
  stdio: ["inherit", "inherit", "inherit", "ipc"],
14
14
  });
15
15
  setCurrentChild(child);
@@ -70,7 +70,7 @@ function runWorker(binPath, args, healthTimeoutMs, setCurrentChild, onHealthy) {
70
70
  });
71
71
  }
72
72
  /** Runs the worker and owns update activation, health checking, and rollback. */
73
- async function superviseRuntime(configDir, initialBinPath, workerArgs = [], healthTimeoutMs = 120_000) {
73
+ async function superviseRuntime(configDir, initialBinPath, workerArgs = [], healthTimeoutMs = 120_000, workerEnv = {}) {
74
74
  let currentBin = initialBinPath;
75
75
  let rollbackBin = null;
76
76
  let candidatePending = null;
@@ -99,7 +99,7 @@ async function superviseRuntime(configDir, initialBinPath, workerArgs = [], heal
99
99
  }
100
100
  while (!stopping) {
101
101
  const candidateThisRun = awaitingCandidateHealth;
102
- const result = await runWorker(currentBin, workerArgs, candidateThisRun ? healthTimeoutMs : null, (child) => {
102
+ const result = await runWorker(currentBin, workerArgs, candidateThisRun ? healthTimeoutMs : null, workerEnv, (child) => {
103
103
  currentChild = child;
104
104
  }, async () => {
105
105
  if (candidateThisRun) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Navarch machine-side session manager: registers a machine, claims tasks from the control-plane dispatcher, runs them via the Claude Code or Codex adapter, and reports results back.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",