@sagentlab/navarch-runtime 0.1.0
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 +351 -0
- package/bin/navarch.cjs +12 -0
- package/dist/adapter.cjs +22 -0
- package/dist/adapters/claude.cjs +135 -0
- package/dist/adapters/codex.cjs +167 -0
- package/dist/adapters/index.cjs +32 -0
- package/dist/adapters/types.cjs +2 -0
- package/dist/api.cjs +140 -0
- package/dist/capacity.cjs +41 -0
- package/dist/claim-loop.cjs +64 -0
- package/dist/cli.cjs +203 -0
- package/dist/config.cjs +55 -0
- package/dist/exit-conditions.cjs +191 -0
- package/dist/heartbeat-loop.cjs +47 -0
- package/dist/logger.cjs +20 -0
- package/dist/machine-store.cjs +58 -0
- package/dist/mcp-config.cjs +41 -0
- package/dist/prompt.cjs +33 -0
- package/dist/redact.cjs +61 -0
- package/dist/sandbox.cjs +166 -0
- package/dist/session.cjs +196 -0
- package/dist/types.cjs +19 -0
- package/dist/upload.cjs +19 -0
- package/package.json +50 -0
package/dist/session.cjs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runSession = runSession;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
10
|
+
const index_cjs_1 = require("./adapters/index.cjs");
|
|
11
|
+
const exit_conditions_cjs_1 = require("./exit-conditions.cjs");
|
|
12
|
+
const redact_cjs_1 = require("./redact.cjs");
|
|
13
|
+
const upload_cjs_1 = require("./upload.cjs");
|
|
14
|
+
const prompt_cjs_1 = require("./prompt.cjs");
|
|
15
|
+
const mcp_config_cjs_1 = require("./mcp-config.cjs");
|
|
16
|
+
const logger_cjs_1 = require("./logger.cjs");
|
|
17
|
+
/** Filename the generated platform MCP config is written under, inside the session's workDir (host-side; also /workspace inside the Docker sandbox -- see sandbox.cts's bind mount). */
|
|
18
|
+
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
19
|
+
const log = (0, logger_cjs_1.createLogger)("session");
|
|
20
|
+
/**
|
|
21
|
+
* Runs one claimed task end to end (implementation-plan.md WP-07):
|
|
22
|
+
* 1. write the prompt file
|
|
23
|
+
* 2. fetch secrets from the broker once, at session start
|
|
24
|
+
* 3. stand up a Docker sandbox, clone the repo, inject env vars
|
|
25
|
+
* 4. run the configured agent adapter (Claude Code or Codex, per
|
|
26
|
+
* FLOTILLA_AGENT — adapters/index.cts#selectAdapter), heartbeating the
|
|
27
|
+
* lease throughout
|
|
28
|
+
* 5. redact + upload the transcript, map the exit condition, complete the
|
|
29
|
+
* lease (recording which agent_type ran it)
|
|
30
|
+
* 6. wipe the sandbox unconditionally
|
|
31
|
+
*
|
|
32
|
+
* NEEDS LIVE VERIFICATION: the full path requires a real Docker daemon, a
|
|
33
|
+
* real `claude` (or `codex`) binary, and a live control-plane API — none of
|
|
34
|
+
* which are available in this offline build environment. Unit tests exercise
|
|
35
|
+
* each collaborator (api.cts, sandbox.cts, exit-conditions.cts, redact.cts,
|
|
36
|
+
* adapters/*.cts) in isolation instead; see runtime/README.md.
|
|
37
|
+
*/
|
|
38
|
+
async function runSession(deps, claimed, sessionId) {
|
|
39
|
+
const { api, config } = deps;
|
|
40
|
+
const { lease_id: leaseId, task, context_bundle: bundle } = claimed;
|
|
41
|
+
// The session's identity is the pre-allocated session id sent at claim time
|
|
42
|
+
// (recorded on the lease by the dispatcher). Lease-scoped API calls
|
|
43
|
+
// (heartbeat/complete/issue/transcript) still key on leaseId.
|
|
44
|
+
const workDir = node_path_1.default.join(config.workspaceRoot, sessionId);
|
|
45
|
+
await node_fs_1.promises.mkdir(workDir, { recursive: true });
|
|
46
|
+
const promptText = (0, prompt_cjs_1.renderPrompt)(task, bundle);
|
|
47
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
|
|
48
|
+
// Secrets: fetched once, held only in memory (registry + env map below),
|
|
49
|
+
// never written to disk on the host. They only ever reach disk inside the
|
|
50
|
+
// sandbox's tmpfs (sandbox.cts injectEnv), which is wiped with the container.
|
|
51
|
+
const registry = new redact_cjs_1.SecretRegistry();
|
|
52
|
+
let secrets = {};
|
|
53
|
+
const secretNames = bundle.secret_manifest.map((s) => s.name);
|
|
54
|
+
if (secretNames.length > 0) {
|
|
55
|
+
const issued = await api.issueSecrets({ lease_id: leaseId, secret_names: secretNames });
|
|
56
|
+
secrets = issued.secrets;
|
|
57
|
+
registry.registerAll(secrets);
|
|
58
|
+
}
|
|
59
|
+
const abortController = new AbortController();
|
|
60
|
+
let leaseLost = false;
|
|
61
|
+
const heartbeatTimer = setInterval(() => {
|
|
62
|
+
api.heartbeatLease(leaseId).catch((err) => {
|
|
63
|
+
log.warn(`lease heartbeat failed for ${leaseId}: ${String(err)} — killing session.`);
|
|
64
|
+
leaseLost = true;
|
|
65
|
+
abortController.abort();
|
|
66
|
+
});
|
|
67
|
+
}, config.leaseHeartbeatIntervalMs);
|
|
68
|
+
const dockerAvailable = config.sandboxMode === "docker" && (await (0, sandbox_cjs_1.isDockerAvailable)());
|
|
69
|
+
if (config.sandboxMode === "docker" && !dockerAvailable) {
|
|
70
|
+
clearInterval(heartbeatTimer);
|
|
71
|
+
log.warn("Docker requested but not available on this machine — failing the task instead of crashing.");
|
|
72
|
+
await api
|
|
73
|
+
.completeLease(leaseId, {
|
|
74
|
+
status: "failed",
|
|
75
|
+
report: "Docker sandbox unavailable on this machine.",
|
|
76
|
+
evidence_urls: [],
|
|
77
|
+
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
78
|
+
exit_status: "crashed",
|
|
79
|
+
agent_type: config.agentType,
|
|
80
|
+
})
|
|
81
|
+
.catch((err) => log.warn(`complete() after docker-unavailable also failed: ${String(err)}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const sandbox = dockerAvailable
|
|
85
|
+
? new sandbox_cjs_1.DockerSandbox({ sessionId, workspaceRoot: config.workspaceRoot, image: config.dockerImage })
|
|
86
|
+
: null;
|
|
87
|
+
// Platform MCP config (implementation-plan.md WP-07: "--mcp-config
|
|
88
|
+
// pointing at the platform MCP server"): generated fresh per session,
|
|
89
|
+
// carrying this machine's bearer token and this session's lease id (the
|
|
90
|
+
// auth app/api/mcp/route.ts requires -- see lib/flotilla/mcp/context.ts),
|
|
91
|
+
// unless the operator pinned a static override via FLOTILLA_MCP_CONFIG_PATH
|
|
92
|
+
// (e.g. pointing at a fake MCP server in local testing). Written to
|
|
93
|
+
// workDir (host path) so it also lands at /workspace/mcp-config.json
|
|
94
|
+
// inside the Docker sandbox (sandbox.cts binds workDir at /workspace) --
|
|
95
|
+
// the selected adapter (adapters/claude.cts or adapters/codex.cts) needs a
|
|
96
|
+
// path valid in whichever environment it actually runs.
|
|
97
|
+
let mcpConfigPath = config.mcpConfigPath;
|
|
98
|
+
if (!mcpConfigPath) {
|
|
99
|
+
const mcpConfig = (0, mcp_config_cjs_1.buildNavarchMcpConfig)({
|
|
100
|
+
apiBase: api.getBaseUrl(),
|
|
101
|
+
machineToken: api.getToken() ?? "",
|
|
102
|
+
leaseId,
|
|
103
|
+
});
|
|
104
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, MCP_CONFIG_FILENAME), JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
105
|
+
mcpConfigPath = sandbox ? `/workspace/${MCP_CONFIG_FILENAME}` : node_path_1.default.join(workDir, MCP_CONFIG_FILENAME);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
if (sandbox) {
|
|
109
|
+
await sandbox.create();
|
|
110
|
+
await sandbox.injectEnv(toEnvMap(secrets));
|
|
111
|
+
await sandbox.cloneRepo(task.repo, Boolean(secrets["github-pat"]));
|
|
112
|
+
}
|
|
113
|
+
// Picks the Claude Code or Codex adapter per FLOTILLA_AGENT
|
|
114
|
+
// (config.cts's `agentType`) — see adapters/index.cts#selectAdapter.
|
|
115
|
+
// Both adapters implement the same AgentAdapter.run() shape
|
|
116
|
+
// (adapters/types.cts), so nothing else in this function branches on
|
|
117
|
+
// which agent is running.
|
|
118
|
+
const adapter = (0, index_cjs_1.selectAdapter)(config.agentType);
|
|
119
|
+
const bin = config.agentType === "codex" ? config.codexBin : config.claudeBin;
|
|
120
|
+
const extraArgs = config.agentType === "codex" ? config.codexExtraArgs : config.claudeExtraArgs;
|
|
121
|
+
const result = await adapter.run({
|
|
122
|
+
prompt: promptText,
|
|
123
|
+
mcpConfigPath,
|
|
124
|
+
bin,
|
|
125
|
+
extraArgs,
|
|
126
|
+
timeoutMs: config.sessionTimeoutMs,
|
|
127
|
+
env: secrets,
|
|
128
|
+
cwd: sandbox ? undefined : workDir,
|
|
129
|
+
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
130
|
+
signal: abortController.signal,
|
|
131
|
+
});
|
|
132
|
+
const mapping = (0, exit_conditions_cjs_1.mapExitCondition)({ ...result, killedByLeaseLoss: leaseLost || result.killedByLeaseLoss });
|
|
133
|
+
const knownSecrets = registry.list();
|
|
134
|
+
const transcript = [
|
|
135
|
+
"# stdout",
|
|
136
|
+
(0, redact_cjs_1.redactText)(result.stdout, knownSecrets),
|
|
137
|
+
"",
|
|
138
|
+
"# stderr",
|
|
139
|
+
(0, redact_cjs_1.redactText)(result.stderr, knownSecrets),
|
|
140
|
+
"",
|
|
141
|
+
].join("\n");
|
|
142
|
+
let transcriptUrl;
|
|
143
|
+
try {
|
|
144
|
+
const { upload_url, public_url } = await api.getTranscriptUploadUrl(leaseId);
|
|
145
|
+
await (0, upload_cjs_1.uploadTranscript)(upload_url, transcript);
|
|
146
|
+
transcriptUrl = public_url;
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
log.warn(`transcript upload failed for ${leaseId}: ${String(err)}`);
|
|
150
|
+
}
|
|
151
|
+
await api.completeLease(leaseId, {
|
|
152
|
+
status: mapping.leaseOutcome,
|
|
153
|
+
report: (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets),
|
|
154
|
+
evidence_urls: mapping.evidenceUrls,
|
|
155
|
+
cost: {
|
|
156
|
+
tokens_in: result.tokensIn ?? 0,
|
|
157
|
+
tokens_out: result.tokensOut ?? 0,
|
|
158
|
+
cost_usd: result.costUsd ?? 0,
|
|
159
|
+
},
|
|
160
|
+
transcript_url: transcriptUrl,
|
|
161
|
+
exit_status: mapping.exitStatus,
|
|
162
|
+
agent_type: config.agentType,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
log.error(`session ${leaseId} threw before completing: ${String(err)}`);
|
|
167
|
+
await api
|
|
168
|
+
.completeLease(leaseId, {
|
|
169
|
+
status: "failed",
|
|
170
|
+
report: (0, redact_cjs_1.redactText)(`Session crashed: ${String(err)}`, registry.list()),
|
|
171
|
+
evidence_urls: [],
|
|
172
|
+
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
173
|
+
exit_status: "crashed",
|
|
174
|
+
agent_type: config.agentType,
|
|
175
|
+
})
|
|
176
|
+
.catch((completeErr) => log.warn(`complete() after crash also failed: ${String(completeErr)}`));
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
clearInterval(heartbeatTimer);
|
|
180
|
+
secrets = {};
|
|
181
|
+
if (sandbox)
|
|
182
|
+
await sandbox.wipe();
|
|
183
|
+
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Uppercases + sanitizes secret names into shell-safe env var names for injectEnv(). */
|
|
187
|
+
function toEnvMap(secrets) {
|
|
188
|
+
const out = {};
|
|
189
|
+
for (const [name, value] of Object.entries(secrets)) {
|
|
190
|
+
out[name.toUpperCase().replace(/[^A-Z0-9_]/g, "_")] = value;
|
|
191
|
+
}
|
|
192
|
+
if (secrets["github-pat"] && !out.GITHUB_TOKEN) {
|
|
193
|
+
out.GITHUB_TOKEN = secrets["github-pat"];
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
package/dist/types.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Navarch runtime — shared wire types.
|
|
3
|
+
//
|
|
4
|
+
// These interfaces mirror the JSON shapes described in:
|
|
5
|
+
// - docs/flotilla/schema-design.md (table columns => API field names, esp. §4, §5, §7)
|
|
6
|
+
// - docs/flotilla/implementation-plan.md (WP-07 behavioral contract)
|
|
7
|
+
// - docs/agent-platform-project-plan.md (§3.8 dispatch/§3.9 adapter contract)
|
|
8
|
+
//
|
|
9
|
+
// The control-plane API is being built in parallel (WP-01/WP-04/WP-05) in
|
|
10
|
+
// other worktrees this agent cannot see. Fields/endpoints marked "ASSUMED"
|
|
11
|
+
// are not pinned down by an explicit route contract in the docs as written
|
|
12
|
+
// and were inferred from the closest analogous shape; see the WP-07 report
|
|
13
|
+
// for the full list of assumptions to confirm once those WPs land. Everything
|
|
14
|
+
// else is quoted close to verbatim from schema-design.md.
|
|
15
|
+
//
|
|
16
|
+
// runtime/src/api.cts is the ONLY place that turns these types into HTTP
|
|
17
|
+
// calls, so reconciling an assumption against the real contract is a
|
|
18
|
+
// same-file edit, not a rewrite.
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/upload.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.uploadTranscript = uploadTranscript;
|
|
4
|
+
/**
|
|
5
|
+
* PUTs an already-redacted transcript to the signed Supabase Storage URL the
|
|
6
|
+
* control plane hands back (schema-design.md §4 sessions.transcript_url;
|
|
7
|
+
* implementation-plan.md WP-07). Kept as its own module so the storage
|
|
8
|
+
* mechanism is a one-function swap if it ever changes.
|
|
9
|
+
*/
|
|
10
|
+
async function uploadTranscript(uploadUrl, content, fetchImpl = fetch) {
|
|
11
|
+
const response = await fetchImpl(uploadUrl, {
|
|
12
|
+
method: "PUT",
|
|
13
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
14
|
+
body: content,
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`Transcript upload failed with ${response.status}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sagentlab/navarch-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Navarch machine-side session manager: registers a machine, claims tasks from the control-plane dispatcher, runs them in a Docker sandbox via the Claude Code or Codex adapter, and reports results back.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/sagentlab/navarch.git",
|
|
10
|
+
"directory": "runtime"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://www.sagentlab.com/navarch",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"navarch",
|
|
15
|
+
"ai-agent",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"codex",
|
|
18
|
+
"task-runner"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"navarch-runtime": "./bin/navarch.cjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"bin",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"prepublishOnly": "npm run build && npm test",
|
|
40
|
+
"start": "node dist/cli.cjs start",
|
|
41
|
+
"register": "node dist/cli.cjs register",
|
|
42
|
+
"connect": "node dist/cli.cjs connect",
|
|
43
|
+
"doctor": "node dist/cli.cjs doctor"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.14.0",
|
|
47
|
+
"typescript": "^5.7.2",
|
|
48
|
+
"vitest": "^2.1.8"
|
|
49
|
+
}
|
|
50
|
+
}
|