@sagentlab/navarch-runtime 0.1.33 → 0.1.35

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
3
+ exports.runPiAdapter = exports.piAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
4
4
  exports.selectAdapter = selectAdapter;
5
5
  const claude_cjs_1 = require("./claude.cjs");
6
6
  Object.defineProperty(exports, "claudeCodeAdapter", { enumerable: true, get: function () { return claude_cjs_1.claudeCodeAdapter; } });
@@ -14,6 +14,9 @@ Object.defineProperty(exports, "runGeminiAdapter", { enumerable: true, get: func
14
14
  const opencode_cjs_1 = require("./opencode.cjs");
15
15
  Object.defineProperty(exports, "openCodeAdapter", { enumerable: true, get: function () { return opencode_cjs_1.openCodeAdapter; } });
16
16
  Object.defineProperty(exports, "runOpenCodeAdapter", { enumerable: true, get: function () { return opencode_cjs_1.runOpenCodeAdapter; } });
17
+ const pi_cjs_1 = require("./pi.cjs");
18
+ Object.defineProperty(exports, "piAdapter", { enumerable: true, get: function () { return pi_cjs_1.piAdapter; } });
19
+ Object.defineProperty(exports, "runPiAdapter", { enumerable: true, get: function () { return pi_cjs_1.runPiAdapter; } });
17
20
  /**
18
21
  * Picks the AgentAdapter (adapters/types.cts) session.cts should run a
19
22
  * session with, keyed off config.cts's `agentType` (NAVARCH_AGENT). This is
@@ -31,6 +34,11 @@ function selectAdapter(agentType) {
31
34
  return opencode_cjs_1.openCodeAdapter;
32
35
  case "claude-code":
33
36
  return claude_cjs_1.claudeCodeAdapter;
37
+ case "pi":
38
+ // Managed hosted runtime (#885): reachable only on a managed runner
39
+ // executing a `runtime: "pi"` claim — config.cts never produces "pi"
40
+ // from NAVARCH_AGENT, so a BYO machine cannot select it locally.
41
+ return pi_cjs_1.piAdapter;
34
42
  default: {
35
43
  // Exhaustiveness guard: config.cts only ever produces the values
36
44
  // above, but fall back to Claude Code rather than throwing if this
@@ -0,0 +1,367 @@
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.piAdapter = exports.HOSTED_PI_MAX_PROVIDER_TURNS = exports.DEFAULT_HOSTED_PI_MODEL = exports.HOSTED_PI_MODELS = exports.DEEPSEEK_API_KEY_ENV = exports.PI_PROVIDER_NAME = void 0;
7
+ exports.buildHostedPiBudgetExtension = buildHostedPiBudgetExtension;
8
+ exports.resolveHostedPiModel = resolveHostedPiModel;
9
+ exports.buildPiProviderConfig = buildPiProviderConfig;
10
+ exports.parsePiJsonEvents = parsePiJsonEvents;
11
+ exports.runPiAdapter = runPiAdapter;
12
+ const node_child_process_1 = require("node:child_process");
13
+ const node_fs_1 = require("node:fs");
14
+ const node_os_1 = __importDefault(require("node:os"));
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ /**
17
+ * Headless Pi coding-agent adapter (#885) — the process-side twin of the
18
+ * Cloudflare hosted executor (worker/hosted-pi/lifecycle.ts). It invokes
19
+ * `pi --mode json -p <prompt>` against DeepSeek V4 through a generated
20
+ * OpenAI-compatible custom-provider config and normalizes the JSON event
21
+ * stream into the common AdapterResult boundary.
22
+ *
23
+ * Hosted Pi is control-plane provisioned (docs/navarch/runtime-adapter-contract.md
24
+ * — "Hosted Pi is not a fifth AgentAdapter" for BYO workers): config.cts
25
+ * never selects this adapter from NAVARCH_AGENT, and dispatch never returns
26
+ * `runtime: "pi"` to a BYO machine. This module exists so the managed runner
27
+ * has the same normalized process boundary as every other CLI, with one
28
+ * shared parsing/provider-config contract.
29
+ *
30
+ * Secret hygiene: the generated models.json references the DeepSeek key ONLY
31
+ * as the `DEEPSEEK_API_KEY` environment-variable name (Pi resolves it from
32
+ * the child environment at request time). The literal key never appears in
33
+ * argv, in the config file, or in any file that survives cleanup. A missing
34
+ * DEEPSEEK_API_KEY in the session environment is a hard configuration
35
+ * failure — the adapter refuses to launch rather than fall back to an
36
+ * ambient credential.
37
+ *
38
+ * MCP: Pi has no MCP client, so `mcpConfigPath` is a documented unsupported
39
+ * control for this adapter (contract: "document an unsupported control
40
+ * instead of inventing one"); platform MCP tools are unavailable to hosted
41
+ * Pi sessions.
42
+ */
43
+ // ---------------------------------------------------------------------------
44
+ // Provider config — MIRROR of worker/hosted-pi/provider-config.ts. Both
45
+ // sides must keep the same provider name, env reference, and model ids.
46
+ // ---------------------------------------------------------------------------
47
+ exports.PI_PROVIDER_NAME = "deepseek";
48
+ exports.DEEPSEEK_API_KEY_ENV = "DEEPSEEK_API_KEY";
49
+ exports.HOSTED_PI_MODELS = ["deepseek-v4-flash", "deepseek-v4-pro"];
50
+ exports.DEFAULT_HOSTED_PI_MODEL = "deepseek-v4-flash";
51
+ exports.HOSTED_PI_MAX_PROVIDER_TURNS = 3;
52
+ function buildHostedPiBudgetExtension() {
53
+ return `export default function navarchHostedPiBudget(pi) {
54
+ let providerTurns = 0;
55
+ pi.on("turn_start", (_event, ctx) => {
56
+ providerTurns += 1;
57
+ if (providerTurns > ${exports.HOSTED_PI_MAX_PROVIDER_TURNS}) ctx.abort();
58
+ });
59
+ }
60
+ `;
61
+ }
62
+ function resolveHostedPiModel(configured) {
63
+ const trimmed = (configured ?? "").trim();
64
+ const bare = trimmed.startsWith(`${exports.PI_PROVIDER_NAME}/`)
65
+ ? trimmed.slice(exports.PI_PROVIDER_NAME.length + 1)
66
+ : trimmed;
67
+ return exports.HOSTED_PI_MODELS.includes(bare)
68
+ ? bare
69
+ : exports.DEFAULT_HOSTED_PI_MODEL;
70
+ }
71
+ /**
72
+ * Pi custom-provider models.json. Cost figures mirror the OFF-PEAK DeepSeek
73
+ * prices from lib/navarch/hosted-credits.ts and only drive Pi's local cost
74
+ * display; billing authority is the control plane's immutable price
75
+ * snapshot, never this file.
76
+ */
77
+ function buildPiProviderConfig() {
78
+ const models = [
79
+ {
80
+ id: "deepseek-v4-flash",
81
+ name: "DeepSeek V4 Flash",
82
+ reasoning: true,
83
+ input: ["text"],
84
+ cost: { input: 0.22, output: 0.66, cacheRead: 0.007, cacheWrite: 0.22 },
85
+ contextWindow: 128_000,
86
+ maxTokens: 16_384,
87
+ },
88
+ {
89
+ id: "deepseek-v4-pro",
90
+ name: "DeepSeek V4 Pro",
91
+ reasoning: true,
92
+ input: ["text"],
93
+ cost: { input: 0.66, output: 1.98, cacheRead: 0.022, cacheWrite: 0.66 },
94
+ contextWindow: 128_000,
95
+ maxTokens: 16_384,
96
+ },
97
+ ];
98
+ return `${JSON.stringify({
99
+ providers: {
100
+ [exports.PI_PROVIDER_NAME]: {
101
+ baseUrl: "https://api.deepseek.com",
102
+ api: "openai-completions",
103
+ apiKey: exports.DEEPSEEK_API_KEY_ENV,
104
+ models,
105
+ },
106
+ },
107
+ }, null, 2)}\n`;
108
+ }
109
+ function tokenCount(value) {
110
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
111
+ ? Math.floor(value)
112
+ : undefined;
113
+ }
114
+ function usdAmount(value) {
115
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
116
+ }
117
+ function extractText(content) {
118
+ if (typeof content === "string")
119
+ return content;
120
+ if (!Array.isArray(content))
121
+ return "";
122
+ return content
123
+ .map((block) => {
124
+ if (typeof block === "string")
125
+ return block;
126
+ if (block &&
127
+ typeof block === "object" &&
128
+ block.type === "text" &&
129
+ typeof block.text === "string") {
130
+ return block.text;
131
+ }
132
+ return "";
133
+ })
134
+ .filter(Boolean)
135
+ .join("\n");
136
+ }
137
+ function parsePiJsonEvents(stdout) {
138
+ const events = [];
139
+ for (const line of stdout.split(/\r?\n/)) {
140
+ const trimmed = line.trim();
141
+ if (!trimmed.startsWith("{"))
142
+ continue;
143
+ try {
144
+ const parsed = JSON.parse(trimmed);
145
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
146
+ events.push(parsed);
147
+ }
148
+ }
149
+ catch {
150
+ // Unknown lines remain available in the raw transcript.
151
+ }
152
+ }
153
+ return events;
154
+ }
155
+ function attachPiOutput(result) {
156
+ const events = parsePiJsonEvents(result.stdout);
157
+ if (events.length === 0)
158
+ return result;
159
+ let cacheHit = 0;
160
+ let cacheMiss = 0;
161
+ let output = 0;
162
+ let costUsd;
163
+ let sawUsage = false;
164
+ let reportText = "";
165
+ for (const event of events) {
166
+ const message = event.message;
167
+ if (event.type !== "message_end" || !message || message.role !== "assistant")
168
+ continue;
169
+ const text = extractText(message.content);
170
+ if (text.trim())
171
+ reportText = text.trim();
172
+ const usage = message.usage;
173
+ if (usage && typeof usage === "object") {
174
+ const input = tokenCount(usage.input);
175
+ const read = tokenCount(usage.cacheRead);
176
+ const write = tokenCount(usage.cacheWrite);
177
+ const out = tokenCount(usage.output);
178
+ if (input !== undefined || read !== undefined || write !== undefined || out !== undefined) {
179
+ sawUsage = true;
180
+ cacheHit += read ?? 0;
181
+ // DeepSeek bills cache writes at the miss rate; cache-miss input is
182
+ // regular input plus cache writes (docs/navarch/hosted-prepaid-credits.md).
183
+ cacheMiss += (input ?? 0) + (write ?? 0);
184
+ output += out ?? 0;
185
+ const total = usage.cost ? usdAmount(usage.cost.total) : undefined;
186
+ if (total !== undefined)
187
+ costUsd = (costUsd ?? 0) + total;
188
+ }
189
+ }
190
+ }
191
+ return {
192
+ ...result,
193
+ ...(sawUsage
194
+ ? {
195
+ // AdapterResult semantics: tokensIn folds every input category
196
+ // together and cacheHitTokensIn is its cache-read component.
197
+ tokensIn: cacheHit + cacheMiss,
198
+ tokensOut: output,
199
+ cacheHitTokensIn: cacheHit,
200
+ }
201
+ : {}),
202
+ ...(costUsd !== undefined ? { costUsd } : {}),
203
+ ...(reportText ? { reportText } : {}),
204
+ };
205
+ }
206
+ // ---------------------------------------------------------------------------
207
+ // Invocation
208
+ // ---------------------------------------------------------------------------
209
+ const CONTAINER_PI_DIR = "/tmp/navarch-pi-agent";
210
+ const BUDGET_EXTENSION_NAME = "navarch-budget.js";
211
+ function withBudgetExtension(args, extensionPath) {
212
+ const promptIndex = args.indexOf("-p");
213
+ if (promptIndex < 0)
214
+ return [...args, "--extension", extensionPath];
215
+ return [...args.slice(0, promptIndex), "--extension", extensionPath, ...args.slice(promptIndex)];
216
+ }
217
+ function hasArg(args, ...flags) {
218
+ return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
219
+ }
220
+ async function runPiAdapter(options) {
221
+ if (!options.env[exports.DEEPSEEK_API_KEY_ENV]) {
222
+ // Fail closed: launching without the referenced managed credential would
223
+ // let Pi fall back to ambient/operator auth outside the session boundary.
224
+ throw new Error(`pi adapter: ${exports.DEEPSEEK_API_KEY_ENV} is missing from the session environment; ` +
225
+ "the managed provider credential must be injected by the hosted runner");
226
+ }
227
+ const args = ["--mode", "json", "--no-extensions"];
228
+ if (!hasArg(options.extraArgs, "--no-session")) {
229
+ // Ephemeral by default: session persistence would leave conversation
230
+ // files (which may quote repository content) behind after cleanup.
231
+ args.push("--no-session");
232
+ }
233
+ args.push(...options.extraArgs);
234
+ const model = resolveHostedPiModel(options.model);
235
+ args.push("--model", `${exports.PI_PROVIDER_NAME}/${model}`);
236
+ if (options.reasoningEffort) {
237
+ // Pi 0.73.1 tops out at xhigh. Navarch's max effort must clamp rather
238
+ // than passing an invalid value that Pi warns about and ignores.
239
+ args.push("--thinking", options.reasoningEffort === "max" ? "xhigh" : options.reasoningEffort);
240
+ }
241
+ args.push("-p", options.prompt);
242
+ const raw = options.dockerExec
243
+ ? await runViaDocker(options, args)
244
+ : await runOnHost(options, args);
245
+ return attachPiOutput(raw);
246
+ }
247
+ async function runOnHost(options, args) {
248
+ // Isolated agent dir so the generated provider config never touches (or
249
+ // reads) an operator's ~/.pi configuration/credentials.
250
+ const agentDir = await node_fs_1.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "navarch-pi-agent-"));
251
+ await node_fs_1.promises.writeFile(node_path_1.default.join(agentDir, "models.json"), buildPiProviderConfig(), { mode: 0o600 });
252
+ const budgetExtensionPath = node_path_1.default.join(agentDir, BUDGET_EXTENSION_NAME);
253
+ await node_fs_1.promises.writeFile(budgetExtensionPath, buildHostedPiBudgetExtension(), {
254
+ mode: 0o600,
255
+ });
256
+ const env = { ...options.env, PI_CODING_AGENT_DIR: agentDir };
257
+ const boundedArgs = withBudgetExtension(args, budgetExtensionPath);
258
+ try {
259
+ return await new Promise((resolve) => {
260
+ let stdout = "";
261
+ let stderr = "";
262
+ let timedOut = false;
263
+ let killedByLeaseLoss = false;
264
+ const child = (0, node_child_process_1.spawn)(options.bin, boundedArgs, {
265
+ cwd: options.cwd,
266
+ env: { ...process.env, ...env },
267
+ });
268
+ child.stdin?.end();
269
+ const timer = setTimeout(() => {
270
+ timedOut = true;
271
+ child.kill("SIGKILL");
272
+ }, options.timeoutMs);
273
+ const onAbort = () => {
274
+ killedByLeaseLoss = true;
275
+ child.kill("SIGKILL");
276
+ };
277
+ options.signal?.addEventListener("abort", onAbort, { once: true });
278
+ child.stdout.on("data", (data) => {
279
+ stdout += data.toString();
280
+ });
281
+ child.stderr.on("data", (data) => {
282
+ stderr += data.toString();
283
+ });
284
+ child.on("error", (err) => {
285
+ clearTimeout(timer);
286
+ options.signal?.removeEventListener("abort", onAbort);
287
+ stderr += `\n${String(err)}`;
288
+ resolve({
289
+ exitCode: null,
290
+ timedOut,
291
+ killedByLeaseLoss,
292
+ stdout,
293
+ stderr,
294
+ });
295
+ });
296
+ child.on("close", (code) => {
297
+ clearTimeout(timer);
298
+ options.signal?.removeEventListener("abort", onAbort);
299
+ resolve({
300
+ exitCode: code,
301
+ timedOut,
302
+ killedByLeaseLoss,
303
+ stdout,
304
+ stderr,
305
+ });
306
+ });
307
+ });
308
+ }
309
+ finally {
310
+ await node_fs_1.promises.rm(agentDir, { recursive: true, force: true }).catch(() => undefined);
311
+ }
312
+ }
313
+ async function runViaDocker(options, args) {
314
+ const { containerName, runner } = options.dockerExec;
315
+ // The provider config carries no secret values (only the DEEPSEEK_API_KEY
316
+ // env reference), so materializing it through the container shell is safe.
317
+ const command = `[ -f /tmp/session.env ] && . /tmp/session.env; cd repo 2>/dev/null; ` +
318
+ `mkdir -p ${CONTAINER_PI_DIR} && cat > ${CONTAINER_PI_DIR}/models.json <<'NAVARCH_PI_EOF'\n` +
319
+ `${buildPiProviderConfig()}NAVARCH_PI_EOF\n` +
320
+ `cat > ${CONTAINER_PI_DIR}/${BUDGET_EXTENSION_NAME} <<'NAVARCH_PI_BUDGET_EOF'\n` +
321
+ `${buildHostedPiBudgetExtension()}NAVARCH_PI_BUDGET_EOF\n` +
322
+ `PI_CODING_AGENT_DIR=${CONTAINER_PI_DIR} ${[
323
+ options.bin,
324
+ ...withBudgetExtension(args, `${CONTAINER_PI_DIR}/${BUDGET_EXTENSION_NAME}`),
325
+ ]
326
+ .map(shellQuote)
327
+ .join(" ")}`;
328
+ let killedByLeaseLoss = false;
329
+ const onAbort = () => {
330
+ killedByLeaseLoss = true;
331
+ runner.run("docker", ["kill", containerName]).catch(() => undefined);
332
+ };
333
+ options.signal?.addEventListener("abort", onAbort, { once: true });
334
+ try {
335
+ const forwardedEnv = Object.keys(options.env).flatMap((name) => ["--env", name]);
336
+ const result = await runner.run("docker", ["exec", ...forwardedEnv, containerName, "sh", "-c", command], {
337
+ timeoutMs: options.timeoutMs,
338
+ env: { ...process.env, ...options.env },
339
+ });
340
+ return {
341
+ exitCode: result.code,
342
+ timedOut: false,
343
+ killedByLeaseLoss,
344
+ stdout: result.stdout,
345
+ stderr: result.stderr,
346
+ };
347
+ }
348
+ catch (err) {
349
+ return {
350
+ exitCode: null,
351
+ timedOut: false,
352
+ killedByLeaseLoss,
353
+ stdout: "",
354
+ stderr: String(err),
355
+ };
356
+ }
357
+ finally {
358
+ options.signal?.removeEventListener("abort", onAbort);
359
+ }
360
+ }
361
+ function shellQuote(value) {
362
+ return `'${value.replace(/'/g, `'\\''`)}'`;
363
+ }
364
+ exports.piAdapter = {
365
+ agentType: "pi",
366
+ run: runPiAdapter,
367
+ };
package/dist/redact.cjs CHANGED
@@ -7,10 +7,14 @@
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.SecretRegistry = void 0;
9
9
  exports.redactText = redactText;
10
+ // Keep in sync with lib/navarch/redaction.ts's SECRET_SHAPE_PATTERNS — the
11
+ // hosted Pi executor (worker/hosted-pi) shares these shapes for its own
12
+ // transcript/progress scrubbing.
10
13
  const SECRET_PATTERNS = [
11
14
  /gh[pousr]_[A-Za-z0-9]{20,}/g, // GitHub PAT / OAuth / user-to-server / refresh tokens
12
15
  /github_pat_[A-Za-z0-9_]{20,}/g, // GitHub fine-grained PAT
13
- /sk-[A-Za-z0-9-]{20,}/g, // generic vendor secret-key style token
16
+ /sk-[A-Za-z0-9-]{20,}/g, // OpenAI-compatible secret keys, incl. DeepSeek `sk-…` (hosted Pi provider)
17
+ /\/\/[^/\s:@]+:[^/\s@]+@/g, // basic-auth userinfo in URLs (e.g. x-access-token:…@github.com clones)
14
18
  /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
15
19
  ];
16
20
  const REDACTED = "[REDACTED]";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through Claude Code, Codex, Gemini, or OpenCode.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",