@sagentlab/navarch-runtime 0.1.33 → 0.1.34

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,321 @@
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.DEFAULT_HOSTED_PI_MODEL = exports.HOSTED_PI_MODELS = exports.DEEPSEEK_API_KEY_ENV = exports.PI_PROVIDER_NAME = void 0;
7
+ exports.resolveHostedPiModel = resolveHostedPiModel;
8
+ exports.buildPiProviderConfig = buildPiProviderConfig;
9
+ exports.parsePiJsonEvents = parsePiJsonEvents;
10
+ exports.runPiAdapter = runPiAdapter;
11
+ const node_child_process_1 = require("node:child_process");
12
+ const node_fs_1 = require("node:fs");
13
+ const node_os_1 = __importDefault(require("node:os"));
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ /**
16
+ * Headless Pi coding-agent adapter (#885) — the process-side twin of the
17
+ * Cloudflare hosted executor (worker/hosted-pi/lifecycle.ts). It invokes
18
+ * `pi --mode json -p <prompt>` against DeepSeek V4 through a generated
19
+ * OpenAI-compatible custom-provider config and normalizes the JSON event
20
+ * stream into the common AdapterResult boundary.
21
+ *
22
+ * Hosted Pi is control-plane provisioned (docs/navarch/runtime-adapter-contract.md
23
+ * — "Hosted Pi is not a fifth AgentAdapter" for BYO workers): config.cts
24
+ * never selects this adapter from NAVARCH_AGENT, and dispatch never returns
25
+ * `runtime: "pi"` to a BYO machine. This module exists so the managed runner
26
+ * has the same normalized process boundary as every other CLI, with one
27
+ * shared parsing/provider-config contract.
28
+ *
29
+ * Secret hygiene: the generated models.json references the DeepSeek key ONLY
30
+ * as a `$DEEPSEEK_API_KEY` environment interpolation (Pi expands it from the
31
+ * child environment at load time). The literal key never appears in argv, in
32
+ * the config file, or in any file that survives cleanup. A missing
33
+ * DEEPSEEK_API_KEY in the session environment is a hard configuration
34
+ * failure — the adapter refuses to launch rather than fall back to an
35
+ * ambient credential.
36
+ *
37
+ * MCP: Pi has no MCP client, so `mcpConfigPath` is a documented unsupported
38
+ * control for this adapter (contract: "document an unsupported control
39
+ * instead of inventing one"); platform MCP tools are unavailable to hosted
40
+ * Pi sessions.
41
+ */
42
+ // ---------------------------------------------------------------------------
43
+ // Provider config — MIRROR of worker/hosted-pi/provider-config.ts. Both
44
+ // sides must keep the same provider name, env reference, and model ids.
45
+ // ---------------------------------------------------------------------------
46
+ exports.PI_PROVIDER_NAME = "deepseek";
47
+ exports.DEEPSEEK_API_KEY_ENV = "DEEPSEEK_API_KEY";
48
+ exports.HOSTED_PI_MODELS = ["deepseek-v4-flash", "deepseek-v4-pro"];
49
+ exports.DEFAULT_HOSTED_PI_MODEL = "deepseek-v4-flash";
50
+ function resolveHostedPiModel(configured) {
51
+ const trimmed = (configured ?? "").trim();
52
+ const bare = trimmed.startsWith(`${exports.PI_PROVIDER_NAME}/`)
53
+ ? trimmed.slice(exports.PI_PROVIDER_NAME.length + 1)
54
+ : trimmed;
55
+ return exports.HOSTED_PI_MODELS.includes(bare)
56
+ ? bare
57
+ : exports.DEFAULT_HOSTED_PI_MODEL;
58
+ }
59
+ /**
60
+ * Pi custom-provider models.json. Cost figures mirror the OFF-PEAK DeepSeek
61
+ * prices from lib/navarch/hosted-credits.ts and only drive Pi's local cost
62
+ * display; billing authority is the control plane's immutable price
63
+ * snapshot, never this file.
64
+ */
65
+ function buildPiProviderConfig() {
66
+ const models = [
67
+ {
68
+ id: "deepseek-v4-flash",
69
+ name: "DeepSeek V4 Flash",
70
+ reasoning: true,
71
+ input: ["text"],
72
+ cost: { input: 0.22, output: 0.66, cacheRead: 0.007, cacheWrite: 0.22 },
73
+ contextWindow: 128_000,
74
+ maxTokens: 16_384,
75
+ },
76
+ {
77
+ id: "deepseek-v4-pro",
78
+ name: "DeepSeek V4 Pro",
79
+ reasoning: true,
80
+ input: ["text"],
81
+ cost: { input: 0.66, output: 1.98, cacheRead: 0.022, cacheWrite: 0.66 },
82
+ contextWindow: 128_000,
83
+ maxTokens: 16_384,
84
+ },
85
+ ];
86
+ return `${JSON.stringify({
87
+ providers: {
88
+ [exports.PI_PROVIDER_NAME]: {
89
+ baseUrl: "https://api.deepseek.com",
90
+ api: "openai-completions",
91
+ apiKey: `$${exports.DEEPSEEK_API_KEY_ENV}`,
92
+ models,
93
+ },
94
+ },
95
+ }, null, 2)}\n`;
96
+ }
97
+ function tokenCount(value) {
98
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
99
+ ? Math.floor(value)
100
+ : undefined;
101
+ }
102
+ function usdAmount(value) {
103
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
104
+ }
105
+ function extractText(content) {
106
+ if (typeof content === "string")
107
+ return content;
108
+ if (!Array.isArray(content))
109
+ return "";
110
+ return content
111
+ .map((block) => {
112
+ if (typeof block === "string")
113
+ return block;
114
+ if (block &&
115
+ typeof block === "object" &&
116
+ block.type === "text" &&
117
+ typeof block.text === "string") {
118
+ return block.text;
119
+ }
120
+ return "";
121
+ })
122
+ .filter(Boolean)
123
+ .join("\n");
124
+ }
125
+ function parsePiJsonEvents(stdout) {
126
+ const events = [];
127
+ for (const line of stdout.split(/\r?\n/)) {
128
+ const trimmed = line.trim();
129
+ if (!trimmed.startsWith("{"))
130
+ continue;
131
+ try {
132
+ const parsed = JSON.parse(trimmed);
133
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
134
+ events.push(parsed);
135
+ }
136
+ }
137
+ catch {
138
+ // Unknown lines remain available in the raw transcript.
139
+ }
140
+ }
141
+ return events;
142
+ }
143
+ function attachPiOutput(result) {
144
+ const events = parsePiJsonEvents(result.stdout);
145
+ if (events.length === 0)
146
+ return result;
147
+ let cacheHit = 0;
148
+ let cacheMiss = 0;
149
+ let output = 0;
150
+ let costUsd;
151
+ let sawUsage = false;
152
+ let reportText = "";
153
+ for (const event of events) {
154
+ const message = event.message;
155
+ if (event.type !== "message_end" || !message || message.role !== "assistant")
156
+ continue;
157
+ const text = extractText(message.content);
158
+ if (text.trim())
159
+ reportText = text.trim();
160
+ const usage = message.usage;
161
+ if (usage && typeof usage === "object") {
162
+ const input = tokenCount(usage.input);
163
+ const read = tokenCount(usage.cacheRead);
164
+ const write = tokenCount(usage.cacheWrite);
165
+ const out = tokenCount(usage.output);
166
+ if (input !== undefined || read !== undefined || write !== undefined || out !== undefined) {
167
+ sawUsage = true;
168
+ cacheHit += read ?? 0;
169
+ // DeepSeek bills cache writes at the miss rate; cache-miss input is
170
+ // regular input plus cache writes (docs/navarch/hosted-prepaid-credits.md).
171
+ cacheMiss += (input ?? 0) + (write ?? 0);
172
+ output += out ?? 0;
173
+ const total = usage.cost ? usdAmount(usage.cost.total) : undefined;
174
+ if (total !== undefined)
175
+ costUsd = (costUsd ?? 0) + total;
176
+ }
177
+ }
178
+ }
179
+ return {
180
+ ...result,
181
+ ...(sawUsage
182
+ ? {
183
+ // AdapterResult semantics: tokensIn folds every input category
184
+ // together and cacheHitTokensIn is its cache-read component.
185
+ tokensIn: cacheHit + cacheMiss,
186
+ tokensOut: output,
187
+ cacheHitTokensIn: cacheHit,
188
+ }
189
+ : {}),
190
+ ...(costUsd !== undefined ? { costUsd } : {}),
191
+ ...(reportText ? { reportText } : {}),
192
+ };
193
+ }
194
+ // ---------------------------------------------------------------------------
195
+ // Invocation
196
+ // ---------------------------------------------------------------------------
197
+ const CONTAINER_PI_DIR = "/tmp/navarch-pi-agent";
198
+ function hasArg(args, ...flags) {
199
+ return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
200
+ }
201
+ async function runPiAdapter(options) {
202
+ if (!options.env[exports.DEEPSEEK_API_KEY_ENV]) {
203
+ // Fail closed: launching without the referenced managed credential would
204
+ // let Pi fall back to ambient/operator auth outside the session boundary.
205
+ throw new Error(`pi adapter: ${exports.DEEPSEEK_API_KEY_ENV} is missing from the session environment; ` +
206
+ "the managed provider credential must be injected by the hosted runner");
207
+ }
208
+ const args = ["--mode", "json"];
209
+ if (!hasArg(options.extraArgs, "--no-session")) {
210
+ // Ephemeral by default: session persistence would leave conversation
211
+ // files (which may quote repository content) behind after cleanup.
212
+ args.push("--no-session");
213
+ }
214
+ args.push(...options.extraArgs);
215
+ const model = resolveHostedPiModel(options.model);
216
+ args.push("--model", `${exports.PI_PROVIDER_NAME}/${model}`);
217
+ if (options.reasoningEffort) {
218
+ // Pi's --thinking accepts off|minimal|low|medium|high|xhigh|max; the
219
+ // resolved efforts are a subset, so they map directly.
220
+ args.push("--thinking", options.reasoningEffort);
221
+ }
222
+ args.push("-p", options.prompt);
223
+ const raw = options.dockerExec
224
+ ? await runViaDocker(options, args)
225
+ : await runOnHost(options, args);
226
+ return attachPiOutput(raw);
227
+ }
228
+ async function runOnHost(options, args) {
229
+ // Isolated agent dir so the generated provider config never touches (or
230
+ // reads) an operator's ~/.pi configuration/credentials.
231
+ const agentDir = await node_fs_1.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "navarch-pi-agent-"));
232
+ await node_fs_1.promises.writeFile(node_path_1.default.join(agentDir, "models.json"), buildPiProviderConfig(), { mode: 0o600 });
233
+ const env = { ...options.env, PI_CODING_AGENT_DIR: agentDir };
234
+ try {
235
+ return await new Promise((resolve) => {
236
+ let stdout = "";
237
+ let stderr = "";
238
+ let timedOut = false;
239
+ let killedByLeaseLoss = false;
240
+ const child = (0, node_child_process_1.spawn)(options.bin, args, {
241
+ cwd: options.cwd,
242
+ env: { ...process.env, ...env },
243
+ });
244
+ child.stdin?.end();
245
+ const timer = setTimeout(() => {
246
+ timedOut = true;
247
+ child.kill("SIGKILL");
248
+ }, options.timeoutMs);
249
+ const onAbort = () => {
250
+ killedByLeaseLoss = true;
251
+ child.kill("SIGKILL");
252
+ };
253
+ options.signal?.addEventListener("abort", onAbort, { once: true });
254
+ child.stdout.on("data", (data) => {
255
+ stdout += data.toString();
256
+ });
257
+ child.stderr.on("data", (data) => {
258
+ stderr += data.toString();
259
+ });
260
+ child.on("error", (err) => {
261
+ clearTimeout(timer);
262
+ options.signal?.removeEventListener("abort", onAbort);
263
+ stderr += `\n${String(err)}`;
264
+ resolve({ exitCode: null, timedOut, killedByLeaseLoss, stdout, stderr });
265
+ });
266
+ child.on("close", (code) => {
267
+ clearTimeout(timer);
268
+ options.signal?.removeEventListener("abort", onAbort);
269
+ resolve({ exitCode: code, timedOut, killedByLeaseLoss, stdout, stderr });
270
+ });
271
+ });
272
+ }
273
+ finally {
274
+ await node_fs_1.promises.rm(agentDir, { recursive: true, force: true }).catch(() => undefined);
275
+ }
276
+ }
277
+ async function runViaDocker(options, args) {
278
+ const { containerName, runner } = options.dockerExec;
279
+ // The provider config carries no secret values (only the $DEEPSEEK_API_KEY
280
+ // env reference), so materializing it through the container shell is safe.
281
+ const command = `[ -f /tmp/session.env ] && . /tmp/session.env; cd repo 2>/dev/null; ` +
282
+ `mkdir -p ${CONTAINER_PI_DIR} && cat > ${CONTAINER_PI_DIR}/models.json <<'NAVARCH_PI_EOF'\n` +
283
+ `${buildPiProviderConfig()}NAVARCH_PI_EOF\n` +
284
+ `PI_CODING_AGENT_DIR=${CONTAINER_PI_DIR} ${[options.bin, ...args].map(shellQuote).join(" ")}`;
285
+ let killedByLeaseLoss = false;
286
+ const onAbort = () => {
287
+ killedByLeaseLoss = true;
288
+ runner.run("docker", ["kill", containerName]).catch(() => undefined);
289
+ };
290
+ options.signal?.addEventListener("abort", onAbort, { once: true });
291
+ try {
292
+ const forwardedEnv = Object.keys(options.env).flatMap((name) => ["--env", name]);
293
+ const result = await runner.run("docker", ["exec", ...forwardedEnv, containerName, "sh", "-c", command], { timeoutMs: options.timeoutMs, env: { ...process.env, ...options.env } });
294
+ return {
295
+ exitCode: result.code,
296
+ timedOut: false,
297
+ killedByLeaseLoss,
298
+ stdout: result.stdout,
299
+ stderr: result.stderr,
300
+ };
301
+ }
302
+ catch (err) {
303
+ return {
304
+ exitCode: null,
305
+ timedOut: false,
306
+ killedByLeaseLoss,
307
+ stdout: "",
308
+ stderr: String(err),
309
+ };
310
+ }
311
+ finally {
312
+ options.signal?.removeEventListener("abort", onAbort);
313
+ }
314
+ }
315
+ function shellQuote(value) {
316
+ return `'${value.replace(/'/g, `'\\''`)}'`;
317
+ }
318
+ exports.piAdapter = {
319
+ agentType: "pi",
320
+ run: runPiAdapter,
321
+ };
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.34",
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",