@sagentlab/navarch-runtime 0.1.7 → 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 +29 -3
- package/bin/git-credential-navarch.cjs +46 -0
- package/dist/cli.cjs +41 -14
- package/dist/config.cjs +2 -0
- package/dist/git-worktree.cjs +93 -30
- package/dist/session.cjs +61 -13
- package/dist/supervisor.cjs +4 -4
- package/package.json +1 -1
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
|
-
|
|
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` |
|
|
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 = (
|
|
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 \`${
|
|
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 = (
|
|
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 \`${
|
|
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 = (
|
|
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 = (
|
|
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
|
-
|
|
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 = (
|
|
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
|
-
|
|
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",
|
package/dist/git-worktree.cjs
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
41
|
+
await this.ensureRepositoryCache();
|
|
42
|
+
try {
|
|
43
|
+
await this.addSessionWorktree();
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
@@ -88,6 +143,8 @@ class GitWorktree {
|
|
|
88
143
|
async runGit(args, authenticated) {
|
|
89
144
|
const credentialArgs = authenticated && this.githubToken
|
|
90
145
|
? [
|
|
146
|
+
"-c",
|
|
147
|
+
"credential.helper=",
|
|
91
148
|
"-c",
|
|
92
149
|
'credential.helper=!f() { echo username=x-access-token; echo "password=$GITHUB_TOKEN"; }; f',
|
|
93
150
|
]
|
|
@@ -170,3 +227,9 @@ function parseRemoteHead(output) {
|
|
|
170
227
|
const match = output.match(/^ref:\s+(refs\/heads\/[A-Za-z0-9._/-]+)\s+HEAD$/m);
|
|
171
228
|
return match?.[1] ?? null;
|
|
172
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
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
+
}
|
package/dist/supervisor.cjs
CHANGED
|
@@ -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.
|
|
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",
|