@offerpilot/axiomruntime 0.0.1 → 0.0.3

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.
@@ -0,0 +1,540 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import lockfile from "proper-lockfile";
7
+ import { resolveToolCommand } from "../runner/command-resolver.js";
8
+ const LOCK_OPTIONS = {
9
+ realpath: false,
10
+ stale: 10_000,
11
+ update: 5_000,
12
+ retries: {
13
+ retries: 100,
14
+ factor: 1.2,
15
+ minTimeout: 25,
16
+ maxTimeout: 500
17
+ }
18
+ };
19
+ const RESERVED_PROVIDER_IDS = new Set(["openai", "ollama", "lmstudio"]);
20
+ const CODEX_PROXY_ENV_KEYS = [
21
+ "HTTP_PROXY",
22
+ "HTTPS_PROXY",
23
+ "ALL_PROXY",
24
+ "http_proxy",
25
+ "https_proxy",
26
+ "all_proxy"
27
+ ];
28
+ export async function setCodexProvider(selection, options = {}) {
29
+ const codexHome = resolveCodexHome(options.codexHome);
30
+ const configPath = path.join(codexHome, "config.toml");
31
+ const authPath = path.join(codexHome, "auth.json");
32
+ await fs.mkdir(codexHome, { recursive: true, mode: 0o700 });
33
+ const release = await lockfile.lock(configPath, LOCK_OPTIONS);
34
+ let providerName;
35
+ let providerId;
36
+ let model;
37
+ try {
38
+ if (selection.type === "chatgpt") {
39
+ providerName = "ChatGPT";
40
+ providerId = "openai";
41
+ model = null;
42
+ await configureChatGpt(configPath, authPath, codexHome, options.runChatGptLogin);
43
+ }
44
+ else {
45
+ providerName = selection.provider.name;
46
+ providerId = providerIdForName(selection.provider.name);
47
+ model = assertCodexGptModel(selection.model);
48
+ await configureCustomProvider(configPath, authPath, selection.provider, providerId, model);
49
+ }
50
+ }
51
+ finally {
52
+ await release();
53
+ }
54
+ const restart = await (options.scheduleRestart ?? scheduleCodexAppRestart)();
55
+ return {
56
+ selection: selection.type,
57
+ providerName,
58
+ providerId,
59
+ model,
60
+ configPath,
61
+ authPath,
62
+ restart
63
+ };
64
+ }
65
+ export async function setCodexProxy(value, options = {}) {
66
+ const proxyUrl = normalizeCodexProxy(value);
67
+ const codexHome = resolveCodexHome(options.codexHome);
68
+ const configPath = path.join(codexHome, "config.toml");
69
+ await fs.mkdir(codexHome, { recursive: true, mode: 0o700 });
70
+ const release = await lockfile.lock(configPath, LOCK_OPTIONS);
71
+ let environment;
72
+ try {
73
+ const snapshot = await snapshotFile(configPath);
74
+ const current = snapshot?.content.toString("utf8") ?? "";
75
+ try {
76
+ if (snapshot || proxyUrl !== null) {
77
+ await atomicWriteFile(configPath, buildCodexProxyConfig(current, proxyUrl), snapshot?.mode ?? 0o600);
78
+ }
79
+ environment = await (options.applyProxyEnvironment
80
+ ?? ((nextProxyUrl) => applyCodexProxyEnvironment(nextProxyUrl, options.platform)))(proxyUrl);
81
+ }
82
+ catch (error) {
83
+ await restoreFile(configPath, snapshot).catch(() => undefined);
84
+ throw error;
85
+ }
86
+ }
87
+ finally {
88
+ await release();
89
+ }
90
+ const restart = await (options.scheduleRestart ?? scheduleCodexAppRestart)();
91
+ return { proxyUrl, configPath, environment, restart };
92
+ }
93
+ export function providerIdForName(name) {
94
+ const trimmed = name.trim();
95
+ if (/^[A-Za-z0-9_-]+$/.test(trimmed) && !RESERVED_PROVIDER_IDS.has(trimmed.toLowerCase())) {
96
+ return trimmed;
97
+ }
98
+ const slug = trimmed
99
+ .normalize("NFKD")
100
+ .replace(/[^A-Za-z0-9_-]+/g, "-")
101
+ .replace(/^-+|-+$/g, "")
102
+ .toLowerCase()
103
+ .slice(0, 40) || "provider";
104
+ const digest = createHash("sha256").update(trimmed).digest("hex").slice(0, 8);
105
+ return `ai-${slug}-${digest}`;
106
+ }
107
+ export function listAvailableCodexGptModels(models) {
108
+ return [...new Set(models.map((model) => model.trim()).filter(isCodexGptModel))]
109
+ .sort(compareCodexGptModelsNewestFirst);
110
+ }
111
+ export function resolveCodexProviderModel(providerName, models, requestedModel) {
112
+ const available = listAvailableCodexGptModels(models);
113
+ if (!available.length) {
114
+ throw new Error(`Provider ${providerName} has no available GPT models. Run \`ai status\` to refresh its model list.`);
115
+ }
116
+ if (!requestedModel)
117
+ return available[0];
118
+ const requested = assertCodexGptModel(requestedModel);
119
+ const selected = available.find((model) => model.toLowerCase() === requested.toLowerCase());
120
+ if (!selected) {
121
+ throw new Error(`GPT model ${requested} is not available for provider ${providerName}. Available: ${available.join(", ")}`);
122
+ }
123
+ return selected;
124
+ }
125
+ export function buildCodexProviderConfig(current, provider, providerId, model = provider.model) {
126
+ if (!provider.baseUrl.trim())
127
+ throw new Error(`Provider ${provider.name} has no base URL.`);
128
+ if (!provider.apiKey.trim())
129
+ throw new Error(`Provider ${provider.name} has no API key.`);
130
+ const selectedModel = assertCodexGptModel(model);
131
+ const eol = current.includes("\r\n") ? "\r\n" : "\n";
132
+ let next = removeProviderTables(current, providerId);
133
+ next = setTopLevelTomlString(next, "cli_auth_credentials_store", "file");
134
+ next = setTopLevelTomlString(next, "model_provider", providerId);
135
+ next = setTopLevelTomlString(next, "model", selectedModel);
136
+ const table = [
137
+ `[model_providers.${providerId}]`,
138
+ `name = ${tomlString(provider.name)}`,
139
+ `base_url = ${tomlString(provider.baseUrl.trim().replace(/\/+$/, ""))}`,
140
+ `env_key = ${tomlString("OPENAI_API_KEY")}`,
141
+ `wire_api = ${tomlString("responses")}`
142
+ ].join(eol);
143
+ return `${next.trimEnd()}${eol}${eol}${table}${eol}`;
144
+ }
145
+ export function buildCodexChatGptConfig(current) {
146
+ let next = setTopLevelTomlString(current, "cli_auth_credentials_store", "file");
147
+ next = setTopLevelTomlString(next, "model_provider", "openai");
148
+ return ensureTrailingNewline(next);
149
+ }
150
+ export function normalizeCodexProxy(value) {
151
+ const trimmed = value.trim();
152
+ if (!trimmed) {
153
+ throw new Error("Proxy address is required. Example: 127.0.0.1:7890");
154
+ }
155
+ if (["off", "none", "clear", "disable"].includes(trimmed.toLowerCase()))
156
+ return null;
157
+ const candidate = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(trimmed)
158
+ ? trimmed
159
+ : `http://${trimmed}`;
160
+ let parsed;
161
+ try {
162
+ parsed = new URL(candidate);
163
+ }
164
+ catch {
165
+ throw new Error(`Invalid proxy address: ${value}`);
166
+ }
167
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
168
+ throw new Error("Codex proxy must use http:// or https://.");
169
+ }
170
+ if (!parsed.hostname)
171
+ throw new Error(`Invalid proxy address: ${value}`);
172
+ if (parsed.username || parsed.password) {
173
+ throw new Error("Proxy credentials are not accepted on the command line; use a local unauthenticated proxy endpoint.");
174
+ }
175
+ if ((parsed.pathname && parsed.pathname !== "/") || parsed.search || parsed.hash) {
176
+ throw new Error("Codex proxy must be an origin only, without a path, query, or fragment.");
177
+ }
178
+ return `${parsed.protocol}//${parsed.host}`;
179
+ }
180
+ export function buildCodexProxyConfig(current, proxyUrl) {
181
+ return updateTomlTableStrings(current, "shell_environment_policy.set", Object.fromEntries(CODEX_PROXY_ENV_KEYS.map((key) => [key, proxyUrl])));
182
+ }
183
+ export async function applyCodexProxyEnvironment(proxyUrl, platform = process.platform) {
184
+ if (platform !== "darwin") {
185
+ return {
186
+ applied: false,
187
+ reason: "Automatic desktop proxy environment updates are currently supported on macOS only."
188
+ };
189
+ }
190
+ const snapshots = new Map();
191
+ for (const key of CODEX_PROXY_ENV_KEYS) {
192
+ const result = await runProcess("/bin/launchctl", ["getenv", key]);
193
+ const previous = result.code === 0 ? result.stdout.replace(/\r?\n$/, "") : "";
194
+ snapshots.set(key, previous || null);
195
+ }
196
+ const changed = [];
197
+ try {
198
+ for (const key of CODEX_PROXY_ENV_KEYS) {
199
+ const args = proxyUrl === null ? ["unsetenv", key] : ["setenv", key, proxyUrl];
200
+ const result = await runProcess("/bin/launchctl", args);
201
+ if (result.code !== 0) {
202
+ throw new Error(`launchctl ${args[0]} failed for ${key}: ${result.stderr.trim() || `exit ${result.code}`}`);
203
+ }
204
+ changed.push(key);
205
+ }
206
+ }
207
+ catch (error) {
208
+ for (const key of changed.reverse()) {
209
+ const previous = snapshots.get(key);
210
+ const args = previous === null || previous === undefined
211
+ ? ["unsetenv", key]
212
+ : ["setenv", key, previous];
213
+ await runProcess("/bin/launchctl", args).catch(() => undefined);
214
+ }
215
+ throw error;
216
+ }
217
+ return { applied: true };
218
+ }
219
+ export async function scheduleCodexAppRestart(options = {}) {
220
+ const platform = options.platform ?? process.platform;
221
+ if (platform !== "darwin") {
222
+ return { scheduled: false, reason: "Automatic Codex app restart is currently supported on macOS only." };
223
+ }
224
+ const homeDir = options.homeDir ?? os.homedir();
225
+ const candidates = options.appCandidates ?? [
226
+ "/Applications/Codex.app",
227
+ "/Applications/ChatGPT.app",
228
+ path.join(homeDir, "Applications/Codex.app"),
229
+ path.join(homeDir, "Applications/ChatGPT.app")
230
+ ];
231
+ const pathExists = options.pathExists ?? fileExists;
232
+ let appPath;
233
+ for (const candidate of candidates) {
234
+ if (await pathExists(candidate)) {
235
+ appPath = candidate;
236
+ break;
237
+ }
238
+ }
239
+ if (!appPath) {
240
+ return { scheduled: false, reason: "Codex.app or ChatGPT.app was not found." };
241
+ }
242
+ const appName = path.basename(appPath, ".app");
243
+ if (!/^[A-Za-z0-9 ._-]+$/.test(appName)) {
244
+ return { scheduled: false, reason: `Unsupported application name: ${appName}` };
245
+ }
246
+ const appleScriptName = appName.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
247
+ const helper = [
248
+ "const { spawnSync } = require('node:child_process');",
249
+ `const appPath = ${JSON.stringify(appPath)};`,
250
+ `const quitScript = ${JSON.stringify(`tell application "${appleScriptName}" to quit`)};`,
251
+ "setTimeout(() => {",
252
+ " spawnSync('/usr/bin/osascript', ['-e', quitScript], { stdio: 'ignore' });",
253
+ " setTimeout(() => spawnSync('/usr/bin/open', [appPath], { stdio: 'ignore' }), 1000);",
254
+ "}, 750);"
255
+ ].join("\n");
256
+ (options.spawnDetached ?? defaultSpawnDetached)(process.execPath, ["-e", helper]);
257
+ return { scheduled: true, appPath };
258
+ }
259
+ async function configureChatGpt(configPath, authPath, codexHome, runner) {
260
+ const [configSnapshot, authSnapshot] = await Promise.all([
261
+ snapshotFile(configPath),
262
+ snapshotFile(authPath)
263
+ ]);
264
+ try {
265
+ const exitCode = await (runner ?? runCodexChatGptLogin)({ codexHome });
266
+ if (exitCode !== 0) {
267
+ throw new Error(`ChatGPT login failed with exit code ${exitCode}.`);
268
+ }
269
+ await validateChatGptAuth(authPath);
270
+ await fs.chmod(authPath, 0o600);
271
+ const currentConfig = configSnapshot?.content.toString("utf8") ?? "";
272
+ await atomicWriteFile(configPath, buildCodexChatGptConfig(currentConfig), configSnapshot?.mode ?? 0o600);
273
+ }
274
+ catch (error) {
275
+ await Promise.allSettled([
276
+ restoreFile(configPath, configSnapshot),
277
+ restoreFile(authPath, authSnapshot)
278
+ ]);
279
+ throw error;
280
+ }
281
+ }
282
+ async function configureCustomProvider(configPath, authPath, provider, providerId, model) {
283
+ const [configSnapshot, authSnapshot] = await Promise.all([
284
+ snapshotFile(configPath),
285
+ snapshotFile(authPath)
286
+ ]);
287
+ const currentConfig = configSnapshot?.content.toString("utf8") ?? "";
288
+ const auth = `${JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: provider.apiKey.trim() }, null, 2)}\n`;
289
+ try {
290
+ await atomicWriteFile(authPath, auth, 0o600);
291
+ await atomicWriteFile(configPath, buildCodexProviderConfig(currentConfig, provider, providerId, model), configSnapshot?.mode ?? 0o600);
292
+ }
293
+ catch (error) {
294
+ await Promise.allSettled([
295
+ restoreFile(configPath, configSnapshot),
296
+ restoreFile(authPath, authSnapshot)
297
+ ]);
298
+ throw error;
299
+ }
300
+ }
301
+ async function runCodexChatGptLogin({ codexHome }) {
302
+ const command = resolveToolCommand("codex");
303
+ if (!command) {
304
+ throw new Error("Codex CLI not found. Run `ai setup --install-tools` first.");
305
+ }
306
+ return new Promise((resolve, reject) => {
307
+ const child = spawn(command, ["-c", "cli_auth_credentials_store=\"file\"", "login"], {
308
+ env: { ...process.env, CODEX_HOME: codexHome },
309
+ stdio: "inherit"
310
+ });
311
+ child.once("error", reject);
312
+ child.once("close", (code, signal) => {
313
+ if (signal) {
314
+ reject(new Error(`ChatGPT login was interrupted by ${signal}.`));
315
+ return;
316
+ }
317
+ resolve(code ?? 1);
318
+ });
319
+ });
320
+ }
321
+ async function validateChatGptAuth(authPath) {
322
+ let auth;
323
+ try {
324
+ auth = JSON.parse(await fs.readFile(authPath, "utf8"));
325
+ }
326
+ catch {
327
+ throw new Error("ChatGPT login completed without a readable Codex auth.json.");
328
+ }
329
+ if (!isRecord(auth) || auth.auth_mode !== "chatgpt" || !isRecord(auth.tokens)) {
330
+ throw new Error("ChatGPT login did not produce valid Codex credentials.");
331
+ }
332
+ if (typeof auth.tokens.access_token !== "string" || typeof auth.tokens.refresh_token !== "string") {
333
+ throw new Error("ChatGPT login credentials are incomplete.");
334
+ }
335
+ }
336
+ function setTopLevelTomlString(current, key, value) {
337
+ const eol = current.includes("\r\n") ? "\r\n" : "\n";
338
+ const lines = current.split(/\r?\n/);
339
+ const firstTable = lines.findIndex((line) => /^\s*\[\[?/.test(line));
340
+ const end = firstTable < 0 ? lines.length : firstTable;
341
+ const assignment = `${key} = ${tomlString(value)}`;
342
+ const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
343
+ let replaced = false;
344
+ const top = lines.slice(0, end).filter((line) => {
345
+ if (!pattern.test(line))
346
+ return true;
347
+ if (replaced)
348
+ return false;
349
+ replaced = true;
350
+ return true;
351
+ });
352
+ if (replaced) {
353
+ const index = top.findIndex((line) => pattern.test(line));
354
+ top[index] = assignment;
355
+ }
356
+ else {
357
+ while (top.length && top[top.length - 1] === "")
358
+ top.pop();
359
+ top.push(assignment, "");
360
+ }
361
+ return [...top, ...lines.slice(end)].join(eol);
362
+ }
363
+ function updateTomlTableStrings(current, tableName, values) {
364
+ const eol = current.includes("\r\n") ? "\r\n" : "\n";
365
+ const lines = current.split(/\r?\n/);
366
+ const tablePattern = new RegExp(`^\\s*\\[${escapeRegExp(tableName)}\\]\\s*(?:#.*)?$`);
367
+ const tableIndex = lines.findIndex((line) => tablePattern.test(line));
368
+ const assignments = Object.entries(values)
369
+ .filter((entry) => entry[1] !== null)
370
+ .map(([key, value]) => `${key} = ${tomlString(value)}`);
371
+ if (tableIndex < 0) {
372
+ if (!assignments.length)
373
+ return ensureTrailingNewline(current);
374
+ const prefix = current.trimEnd();
375
+ const table = [`[${tableName}]`, ...assignments].join(eol);
376
+ return `${prefix ? `${prefix}${eol}${eol}` : ""}${table}${eol}`;
377
+ }
378
+ let tableEnd = lines.length;
379
+ for (let index = tableIndex + 1; index < lines.length; index += 1) {
380
+ if (/^\s*\[\[?/.test(lines[index])) {
381
+ tableEnd = index;
382
+ break;
383
+ }
384
+ }
385
+ const managedPatterns = Object.keys(values).map((key) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`));
386
+ const body = lines
387
+ .slice(tableIndex + 1, tableEnd)
388
+ .filter((line) => !managedPatterns.some((pattern) => pattern.test(line)));
389
+ while (body.length && body[body.length - 1] === "")
390
+ body.pop();
391
+ if (body.length && assignments.length)
392
+ body.push("");
393
+ body.push(...assignments);
394
+ return ensureTrailingNewline([
395
+ ...lines.slice(0, tableIndex + 1),
396
+ ...body,
397
+ ...lines.slice(tableEnd)
398
+ ].join(eol));
399
+ }
400
+ function removeProviderTables(current, providerId) {
401
+ const lines = current.split(/\r?\n/);
402
+ const prefix = `model_providers.${providerId}`;
403
+ let skip = false;
404
+ const kept = [];
405
+ for (const line of lines) {
406
+ const header = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line)?.[1];
407
+ if (header) {
408
+ skip = header === prefix || header.startsWith(`${prefix}.`);
409
+ }
410
+ if (!skip)
411
+ kept.push(line);
412
+ }
413
+ return kept.join(current.includes("\r\n") ? "\r\n" : "\n");
414
+ }
415
+ async function snapshotFile(filePath) {
416
+ try {
417
+ const [content, stat] = await Promise.all([fs.readFile(filePath), fs.stat(filePath)]);
418
+ return { content, mode: stat.mode & 0o777 };
419
+ }
420
+ catch (error) {
421
+ if (error.code === "ENOENT")
422
+ return null;
423
+ throw error;
424
+ }
425
+ }
426
+ async function restoreFile(filePath, snapshot) {
427
+ if (!snapshot) {
428
+ await fs.rm(filePath, { force: true });
429
+ return;
430
+ }
431
+ await atomicWriteFile(filePath, snapshot.content, snapshot.mode);
432
+ }
433
+ async function atomicWriteFile(filePath, content, mode) {
434
+ await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
435
+ const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
436
+ try {
437
+ await fs.writeFile(temporaryPath, content, { mode });
438
+ await fs.chmod(temporaryPath, mode);
439
+ await fs.rename(temporaryPath, filePath);
440
+ }
441
+ catch (error) {
442
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
443
+ throw error;
444
+ }
445
+ }
446
+ function resolveCodexHome(explicit) {
447
+ const value = explicit ?? process.env.CODEX_HOME;
448
+ return value ? path.resolve(value) : path.join(os.homedir(), ".codex");
449
+ }
450
+ function ensureTrailingNewline(value) {
451
+ return `${value.trimEnd()}\n`;
452
+ }
453
+ function tomlString(value) {
454
+ return JSON.stringify(value);
455
+ }
456
+ function escapeRegExp(value) {
457
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
458
+ }
459
+ function assertCodexGptModel(model) {
460
+ const value = model.trim();
461
+ if (!isCodexGptModel(value)) {
462
+ throw new Error(`Codex provider model must be a GPT model id starting with \"gpt-\": ${model || "(empty)"}`);
463
+ }
464
+ return value;
465
+ }
466
+ function isCodexGptModel(model) {
467
+ return /^gpt-/i.test(model.trim());
468
+ }
469
+ function compareCodexGptModelsNewestFirst(left, right) {
470
+ const leftKey = codexGptModelSortKey(left);
471
+ const rightKey = codexGptModelSortKey(right);
472
+ const versionLength = Math.max(leftKey.version.length, rightKey.version.length);
473
+ for (let index = 0; index < versionLength; index += 1) {
474
+ const difference = (rightKey.version[index] ?? -1) - (leftKey.version[index] ?? -1);
475
+ if (difference)
476
+ return difference;
477
+ }
478
+ if (leftKey.tier !== rightKey.tier)
479
+ return rightKey.tier - leftKey.tier;
480
+ if (leftKey.alias !== rightKey.alias)
481
+ return rightKey.alias - leftKey.alias;
482
+ if (leftKey.snapshot !== rightKey.snapshot)
483
+ return rightKey.snapshot - leftKey.snapshot;
484
+ return left.localeCompare(right);
485
+ }
486
+ function codexGptModelSortKey(model) {
487
+ const value = model.toLowerCase();
488
+ const version = /^gpt-(\d+(?:\.\d+)*)/.exec(value)?.[1]
489
+ ?.split(".")
490
+ .map(Number) ?? [];
491
+ const snapshotMatch = /(?:^|-)(\d{4})-(\d{2})-(\d{2})(?:$|-)/.exec(value);
492
+ const snapshot = snapshotMatch
493
+ ? Number(`${snapshotMatch[1]}${snapshotMatch[2]}${snapshotMatch[3]}`)
494
+ : 0;
495
+ const tier = /(?:^|-)sol(?:-|$)/.test(value) ? 60
496
+ : /(?:^|-)pro(?:-|$)/.test(value) ? 50
497
+ : /^gpt-\d+(?:\.\d+)*(?:-\d{4}-\d{2}-\d{2})?$/.test(value) ? 45
498
+ : /(?:^|-)codex(?:-|$)/.test(value) ? 40
499
+ : /(?:^|-)terra(?:-|$)/.test(value) ? 30
500
+ : /(?:^|-)mini(?:-|$)/.test(value) ? 20
501
+ : /(?:^|-)luna(?:-|$)/.test(value) ? 10
502
+ : /(?:^|-)nano(?:-|$)/.test(value) ? 5
503
+ : 0;
504
+ return { version, tier, alias: snapshot ? 0 : 1, snapshot };
505
+ }
506
+ function isRecord(value) {
507
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
508
+ }
509
+ async function fileExists(filePath) {
510
+ try {
511
+ await fs.access(filePath);
512
+ return true;
513
+ }
514
+ catch {
515
+ return false;
516
+ }
517
+ }
518
+ function defaultSpawnDetached(command, args) {
519
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
520
+ child.unref();
521
+ }
522
+ function runProcess(command, args) {
523
+ return new Promise((resolve, reject) => {
524
+ const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
525
+ let stdout = "";
526
+ let stderr = "";
527
+ child.stdout.setEncoding("utf8");
528
+ child.stderr.setEncoding("utf8");
529
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
530
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
531
+ child.once("error", reject);
532
+ child.once("close", (code, signal) => {
533
+ if (signal) {
534
+ reject(new Error(`${command} was interrupted by ${signal}.`));
535
+ return;
536
+ }
537
+ resolve({ code: code ?? 1, stdout, stderr });
538
+ });
539
+ });
540
+ }
@@ -0,0 +1,19 @@
1
+ export const CLAUDE_ROOT_PERMISSION_WARNING = "Claude bypass permissions is unavailable while Axiom Runtime runs as root; using default permissions. Run `ai` as a non-root user to enable auto mode.";
2
+ export function getProcessUid() {
3
+ return typeof process.getuid === "function" ? process.getuid() : null;
4
+ }
5
+ export function isPrivilegedProcess(options = {}) {
6
+ const uid = options.uid === undefined ? getProcessUid() : options.uid;
7
+ return uid === 0;
8
+ }
9
+ export function resolveClaudePermissionMode(requestedMode, options = {}) {
10
+ const privileged = isPrivilegedProcess(options);
11
+ const constrained = privileged && requestedMode === "bypassPermissions";
12
+ return {
13
+ requestedMode,
14
+ effectiveMode: constrained ? "default" : requestedMode,
15
+ privileged,
16
+ constrained,
17
+ reason: constrained ? "root_bypass_permissions_unsupported" : null
18
+ };
19
+ }
@@ -1,3 +1,4 @@
1
+ import { resolveClaudePermissionMode } from "./claude-permission-policy.js";
1
2
  const runnerEngines = new Map();
2
3
  export function registerRunnerEngine(descriptor) {
3
4
  runnerEngines.set(descriptor.name, descriptor);
@@ -30,12 +31,13 @@ registerRunnerEngine({
30
31
  ANTHROPIC_MODEL: model
31
32
  };
32
33
  },
33
- buildArgs: ({ provider, model, settingsPath }) => {
34
+ buildArgs: ({ provider, model, settingsPath, uid }) => {
34
35
  const args = ["--model", model];
35
36
  if (settingsPath) {
36
37
  args.push("--settings", settingsPath);
37
38
  }
38
- if (provider.mode === "auto") {
39
+ const permission = resolveClaudePermissionMode(provider.mode === "auto" ? "bypassPermissions" : "default", { uid });
40
+ if (permission.effectiveMode === "bypassPermissions") {
39
41
  args.push("--permission-mode", "bypassPermissions");
40
42
  args.push("--dangerously-skip-permissions");
41
43
  }
@@ -76,8 +78,6 @@ registerRunnerEngine({
76
78
  "-c",
77
79
  `model_providers.${providerId}.env_key=${tomlString("OPENAI_API_KEY")}`,
78
80
  "-c",
79
- `model_providers.${providerId}.requires_openai_auth=true`,
80
- "-c",
81
81
  `model_providers.${providerId}.wire_api=${tomlString("responses")}`,
82
82
  "--model",
83
83
  model