axexec 1.6.0 → 1.7.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.
Files changed (48) hide show
  1. package/README.md +32 -22
  2. package/dist/agents/copilot/stream-session.js +8 -7
  3. package/dist/agents/copilot/watch-session.js +3 -1
  4. package/dist/agents/opencode/adapter.js +8 -1
  5. package/dist/agents/opencode/build-permission-environment.d.ts +17 -0
  6. package/dist/agents/opencode/build-permission-environment.js +31 -0
  7. package/dist/agents/opencode/parse-sse-event.js +3 -1
  8. package/dist/agents/opencode/spawn-server.js +12 -1
  9. package/dist/build-agent-environment.js +8 -0
  10. package/dist/build-execution-metadata.d.ts +1 -2
  11. package/dist/build-execution-metadata.js +1 -1
  12. package/dist/cli.d.ts +5 -6
  13. package/dist/cli.js +81 -60
  14. package/dist/credentials/get-credential-environment.d.ts +1 -1
  15. package/dist/credentials/get-credential-environment.js +1 -1
  16. package/dist/credentials/install-credentials.d.ts +3 -4
  17. package/dist/credentials/install-credentials.js +5 -3
  18. package/dist/credentials/write-agent-credentials.d.ts +4 -2
  19. package/dist/credentials/write-agent-credentials.js +1 -6
  20. package/dist/execute-agent.d.ts +1 -2
  21. package/dist/execute-agent.js +1 -0
  22. package/dist/format-zod-error.d.ts +6 -0
  23. package/dist/format-zod-error.js +12 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/parse-credentials.d.ts +1 -2
  26. package/dist/parse-credentials.js +7 -6
  27. package/dist/read-credentials-file.d.ts +13 -0
  28. package/dist/read-credentials-file.js +39 -0
  29. package/dist/read-stdin.d.ts +5 -0
  30. package/dist/read-stdin.js +11 -0
  31. package/dist/resolve-credentials.d.ts +21 -0
  32. package/dist/resolve-credentials.js +23 -0
  33. package/dist/resolve-output-mode.d.ts +15 -1
  34. package/dist/resolve-output-mode.js +16 -1
  35. package/dist/resolve-prompt.d.ts +22 -0
  36. package/dist/resolve-prompt.js +23 -0
  37. package/dist/run-agent.d.ts +4 -1
  38. package/dist/run-agent.js +47 -6
  39. package/dist/types/run-result.d.ts +5 -2
  40. package/dist/validate-cwd.d.ts +11 -0
  41. package/dist/validate-cwd.js +24 -0
  42. package/dist/validate-opencode-options.d.ts +19 -0
  43. package/dist/validate-opencode-options.js +58 -0
  44. package/dist/validate-stdin-usage.d.ts +18 -0
  45. package/dist/validate-stdin-usage.js +28 -0
  46. package/package.json +1 -1
  47. package/dist/credentials/credentials.d.ts +0 -39
  48. package/dist/credentials/credentials.js +0 -73
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # axexec
2
2
 
3
- Execution engine for AI coding agents. Translates abstract inputs to agent-specific formats, runs agents in complete isolation, and normalizes output to a standard event stream.
3
+ Execution engine for AI coding agents. Translates abstract inputs to agent-specific formats, runs agents with credential/config isolation (not an OS sandbox), and normalizes output to a standard event stream.
4
4
 
5
5
  ## What axexec Does
6
6
 
@@ -8,7 +8,9 @@ axexec is the **translation layer** between high-level tooling and agent-specifi
8
8
 
9
9
  - **Input translation**: Credentials → agent-specific files, permissions → config files, model → CLI flags
10
10
  - **Output normalization**: Claude JSONL, Codex items, Gemini events, OpenCode SSE → unified `AxexecEvent` stream
11
- - **Complete isolation**: Agents run in temp directories with no access to user config or global credentials
11
+ - **Credential/config isolation**: Agents are pointed at temp directories instead of your local agent config/credential locations
12
+
13
+ axexec is **not** a security boundary. It does not restrict filesystem or network access; many adapters run in permissive non-interactive modes to avoid prompts. For real enforcement, run inside an external sandbox (container/VM).
12
14
 
13
15
  ## Installation
14
16
 
@@ -48,12 +50,15 @@ axexec -a gemini "Refactor the utils module"
48
50
  axexec -a opencode "Add logging"
49
51
  axexec -a copilot "Write tests"
50
52
 
53
+ # Run with explicit credentials JSON (from axauth export)
54
+ axexec -a claude --credentials-file ./creds.json "Review this PR"
55
+
51
56
  # Specify a model
52
57
  axexec -a claude --model opus "Review this PR"
53
58
  axexec -a gemini --model gemini-2.5-pro "Refactor utils"
54
59
 
55
60
  # Set permissions
56
- axexec -a claude --allow 'read,glob,bash:git *' "Check git history"
61
+ axexec -a claude --allow 'read,glob,bash:git *' "Check git history" # best-effort
57
62
 
58
63
  # Output normalized JSONL event stream
59
64
  axexec -a claude -f jsonl "Add tests" | jq 'select(.type == "tool.call")'
@@ -86,11 +91,13 @@ axexec --list-agents | tail -n +2 | awk -F'\t' '$3 ~ /openai/ {print $1}'
86
91
 
87
92
  ```
88
93
  -a, --agent <id> Agent to use (claude, codex, gemini, opencode, copilot)
94
+ --cwd <path> Working directory for the agent process
95
+ --credentials-file <path|-> Read credentials JSON from file (or '-' for stdin)
89
96
  -p, --prompt <text> Prompt text (alternative to positional argument)
90
97
  -m, --model <model> Model to use (agent-specific)
91
- --provider <provider> Provider for OpenCode (anthropic, openai, google)
92
- --allow <perms> Permission rules to allow (comma-separated)
93
- --deny <perms> Permission rules to deny (comma-separated)
98
+ --provider <provider> Provider for OpenCode (e.g., anthropic, openai, google, opencode)
99
+ --allow <perms> Allow permissions (best-effort, comma-separated)
100
+ --deny <perms> Deny permissions (best-effort, comma-separated)
94
101
  -f, --format <fmt> Output format: jsonl, tsv (default: tsv, truncated on TTY)
95
102
  --raw-log <file> Write raw agent output to file
96
103
  --debug Enable debug mode (logs unknown events)
@@ -103,15 +110,15 @@ axexec --list-agents | tail -n +2 | awk -F'\t' '$3 ~ /openai/ {print $1}'
103
110
 
104
111
  ## Supported Agents
105
112
 
106
- | Agent | Package | API Key Env Var |
107
- | -------- | ------------------------- | --------------------- |
108
- | claude | @anthropic-ai/claude-code | ANTHROPIC_API_KEY |
109
- | codex | @openai/codex | OPENAI_API_KEY |
110
- | gemini | @google/gemini-cli | GEMINI_API_KEY |
111
- | opencode | opencode-ai | ANTHROPIC_API_KEY (†) |
112
- | copilot | @github/copilot | GITHUB_TOKEN |
113
+ | Agent | Package | API Key Env Var |
114
+ | -------- | ------------------------- | ----------------------- |
115
+ | claude | @anthropic-ai/claude-code | ANTHROPIC_API_KEY |
116
+ | codex | @openai/codex | OPENAI_API_KEY |
117
+ | gemini | @google/gemini-cli | GEMINI_API_KEY |
118
+ | opencode | opencode-ai | AX_OPENCODE_CREDENTIALS |
119
+ | copilot | @github/copilot | GITHUB_TOKEN |
113
120
 
114
- (†) OpenCode supports multiple providers via `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY`.
121
+ OpenCode requires `AX_OPENCODE_CREDENTIALS` with provider-specific credentials (see [CI/CD Credentials](#cicd-credentials)).
115
122
 
116
123
  ## Event Stream
117
124
 
@@ -170,8 +177,8 @@ For OpenCode, set `AX_OPENCODE_CREDENTIALS` with your credentials. The `provider
170
177
  field must match your `--provider` flag:
171
178
 
172
179
  ```bash
173
- # Anthropic (provider defaults to "anthropic" if omitted)
174
- AX_OPENCODE_CREDENTIALS='{"agent":"opencode","type":"api-key","data":{"apiKey":"sk-ant-..."}}' \
180
+ # Anthropic
181
+ AX_OPENCODE_CREDENTIALS='{"agent":"opencode","type":"api-key","provider":"anthropic","data":{"apiKey":"sk-ant-..."}}' \
175
182
  axexec -a opencode --provider anthropic -m claude-sonnet-4 "Hello"
176
183
 
177
184
  # OpenAI (provider must be specified in credentials)
@@ -185,7 +192,7 @@ AX_OPENCODE_CREDENTIALS='{"agent":"opencode","type":"api-key","provider":"openco
185
192
 
186
193
  ## Isolation
187
194
 
188
- axexec provides complete environment isolation:
195
+ axexec provides credential/config isolation:
189
196
 
190
197
  1. Creates a temp directory for each session
191
198
  2. Installs credentials to the temp directory
@@ -193,11 +200,14 @@ axexec provides complete environment isolation:
193
200
  4. Sets environment variables to point agents at the temp directory
194
201
  5. Cleans up the temp directory after execution
195
202
 
196
- Codex runs with `--dangerously-bypass-approvals-and-sandbox`, so `--allow` /
197
- `--deny` do not constrain Codex. Run axexec inside an external sandbox if you
198
- need enforcement.
203
+ axexec is intentionally **permissive** and should not be treated as a sandbox:
204
+
205
+ - It does not enforce filesystem or network restrictions
206
+ - `--allow` / `--deny` is best-effort and may be ignored or weakened by adapter "YOLO" switches
207
+ - Agent-internal sandbox env vars (`GEMINI_SANDBOX`, `SEATBELT_PROFILE`) are excluded from subprocess environments
208
+ - For enforcement, run axexec inside an external sandbox (container/VM)
199
209
 
200
- Agents **never** access:
210
+ Agents are configured to avoid using your local agent config/credential locations:
201
211
 
202
212
  - User's home directory config (`~/.claude`, `~/.codex`, `~/.gemini`)
203
213
  - System keychain or credential managers
@@ -212,7 +222,7 @@ Add to your `CLAUDE.md` or `AGENTS.md`:
212
222
 
213
223
  Run `npx -y axexec --help` to learn available options.
214
224
 
215
- Use `axexec` when you need to run a supported agent in a fully isolated environment and consume a normalized event stream. It translates credentials and permissions to agent-specific formats so your automation stays consistent across providers.
225
+ Use `axexec` when you need to run a supported agent with credential/config isolation and consume a normalized event stream. It translates credentials and permissions to agent-specific formats so your automation stays consistent across providers.
216
226
  ```
217
227
 
218
228
  ## License
@@ -23,13 +23,9 @@ async function main() {
23
23
  const copilotArguments = [
24
24
  "-p",
25
25
  prompt,
26
- "--allow-all-tools",
27
- // Enable network access without confirmation prompts.
28
- // Copilot requires user approval for URL access by default, but other
29
- // agents (Gemini, OpenCode) allow it by default. For unified behavior,
30
- // enable it. This matches how --allow-all-tools works for other operations.
31
- // See: copilot-cli-decompiled/copilot-source/index.js line 418918
32
- "--allow-all-urls",
26
+ // Always run in "YOLO" mode (no prompts). This is equivalent to
27
+ // --allow-all-tools --allow-all-paths --allow-all-urls.
28
+ "--yolo",
33
29
  ...extraArguments,
34
30
  ];
35
31
  // Record existing session files
@@ -55,6 +51,11 @@ async function main() {
55
51
  const copilotBin = process.env["AXEXEC_COPILOT_PATH"] ?? "copilot";
56
52
  const child = spawn(copilotBin, copilotArguments, {
57
53
  stdio: ["ignore", "pipe", "pipe"],
54
+ env: {
55
+ ...process.env,
56
+ // Auto-trust folders (avoids interactive trust prompts).
57
+ COPILOT_ALLOW_ALL: "true",
58
+ },
58
59
  });
59
60
  copilotProcess = child;
60
61
  // Pipe Copilot's stdout/stderr to our stderr (for debugging)
@@ -13,7 +13,9 @@ import { existsSync, readdirSync, statSync, watch, } from "node:fs";
13
13
  import { homedir } from "node:os";
14
14
  import path from "node:path";
15
15
  /** Copilot session state directory */
16
- const SESSION_STATE_DIR = path.join(homedir(), ".copilot", "session-state");
16
+ const SESSION_STATE_DIR = process.env["XDG_STATE_HOME"]
17
+ ? path.join(process.env["XDG_STATE_HOME"], ".copilot", "session-state")
18
+ : path.join(homedir(), ".copilot", "session-state");
17
19
  /** Session events filename inside session directories (new format) */
18
20
  const SESSION_EVENTS_FILE = "events.jsonl";
19
21
  /**
@@ -14,6 +14,7 @@
14
14
  * 7. Clean up server on completion
15
15
  */
16
16
  import { registerAdapter } from "../registry.js";
17
+ import { buildPermissionEnvironment } from "./build-permission-environment.js";
17
18
  import { cleanupSession } from "./cleanup-session.js";
18
19
  import { createSessionStartEvent } from "./create-session-start-event.js";
19
20
  import { determineSessionSuccess } from "../../determine-session-success.js";
@@ -49,7 +50,13 @@ async function* streamSession(options) {
49
50
  process.on("SIGTERM", forwardSignal);
50
51
  try {
51
52
  // 1. Spawn server
52
- const server = await spawnServer({ cwd, signal, env: options.configEnv });
53
+ const configEnvironment = options.configEnv ?? {};
54
+ const permissionEnvironment = buildPermissionEnvironment(configEnvironment);
55
+ const server = await spawnServer({
56
+ cwd,
57
+ signal,
58
+ env: { ...configEnvironment, ...permissionEnvironment },
59
+ });
53
60
  serverProcess = server.process;
54
61
  serverUrl = server.url;
55
62
  // 2. Connect to SSE and start streaming
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Builds permissive OpenCode environment variables for headless execution.
3
+ *
4
+ * When axconfig hasn't generated an explicit OPENCODE_CONFIG_DIR (via
5
+ * --allow/--deny), this injects allow-all permissions to avoid interactive
6
+ * prompts. OpenCode's default config includes "ask" permissions that would
7
+ * block headless execution.
8
+ */
9
+ /**
10
+ * Returns environment variables for permissive OpenCode execution.
11
+ *
12
+ * Returns an empty object (don't override) if:
13
+ * - `OPENCODE_CONFIG_DIR` is set (axconfig's explicit permissions via --allow/--deny)
14
+ * - `OPENCODE_PERMISSION` is already set (caller's explicit permissions)
15
+ */
16
+ declare function buildPermissionEnvironment(configEnvironment: Record<string, string>): Record<string, string>;
17
+ export { buildPermissionEnvironment };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Builds permissive OpenCode environment variables for headless execution.
3
+ *
4
+ * When axconfig hasn't generated an explicit OPENCODE_CONFIG_DIR (via
5
+ * --allow/--deny), this injects allow-all permissions to avoid interactive
6
+ * prompts. OpenCode's default config includes "ask" permissions that would
7
+ * block headless execution.
8
+ */
9
+ /**
10
+ * Returns environment variables for permissive OpenCode execution.
11
+ *
12
+ * Returns an empty object (don't override) if:
13
+ * - `OPENCODE_CONFIG_DIR` is set (axconfig's explicit permissions via --allow/--deny)
14
+ * - `OPENCODE_PERMISSION` is already set (caller's explicit permissions)
15
+ */
16
+ function buildPermissionEnvironment(configEnvironment) {
17
+ if (configEnvironment["OPENCODE_CONFIG_DIR"] !== undefined ||
18
+ configEnvironment["OPENCODE_PERMISSION"] !== undefined) {
19
+ return {};
20
+ }
21
+ return {
22
+ OPENCODE_PERMISSION: JSON.stringify({
23
+ "*": "allow",
24
+ doom_loop: "allow",
25
+ external_directory: "allow",
26
+ read: "allow",
27
+ question: "allow",
28
+ }),
29
+ };
30
+ }
31
+ export { buildPermissionEnvironment };
@@ -110,7 +110,9 @@ function parseSSEEvent(event, state) {
110
110
  {
111
111
  type: "session.error",
112
112
  code: "PERMISSION_REQUIRED",
113
- message: `OpenCode requires permission: ${tool}. ${title}. Pre-configure permissions in OpenCode settings.`,
113
+ message: `OpenCode requires permission: ${tool}. ${title}. ` +
114
+ `axexec runs OpenCode non-interactively, so permission prompts are treated as errors. ` +
115
+ `Use axexec --allow/--deny to pre-configure permissions.`,
114
116
  timestamp: Date.now(),
115
117
  },
116
118
  ];
@@ -4,6 +4,7 @@
4
4
  * Handles spawning the OpenCode server process and waiting for it to start.
5
5
  */
6
6
  import { spawn } from "node:child_process";
7
+ import { buildBaseEnvironment } from "../../build-agent-environment.js";
7
8
  import { resolveBinary } from "../../resolve-binary.js";
8
9
  /** Error thrown when server fails to start */
9
10
  class ServerStartError extends Error {
@@ -27,10 +28,20 @@ async function spawnServer(options) {
27
28
  environmentVariable: "AXEXEC_OPENCODE_PATH",
28
29
  installHint: "npm install -g opencode-ai",
29
30
  });
31
+ // OpenCode supports several env-based configuration overrides (OPENCODE_*).
32
+ // Exclude them from the host environment so axexec can run with isolated
33
+ // temp config/data directories instead of inheriting user settings.
34
+ const baseEnvironment = buildBaseEnvironment([
35
+ "OPENCODE_CONFIG",
36
+ "OPENCODE_CONFIG_CONTENT",
37
+ "OPENCODE_CONFIG_DIR",
38
+ "OPENCODE_DATA_DIR",
39
+ "OPENCODE_PERMISSION",
40
+ ]);
30
41
  const child = spawn(bin, ["serve", "--port", "0"], {
31
42
  cwd,
32
43
  stdio: ["ignore", "pipe", "pipe"],
33
- env: { ...process.env, ...env },
44
+ env: { ...baseEnvironment, ...env },
34
45
  });
35
46
  let stderrBuffer = "";
36
47
  let resolved = false;
@@ -1,10 +1,18 @@
1
1
  const VAULT_ENV_EXCLUSIONS = ["AXVAULT", "AXVAULT_URL", "AXVAULT_API_KEY"];
2
+ // Prevent upstream CLIs from enabling their own internal sandboxing based on
3
+ // host environment variables. axexec is intentionally insecure-by-default and
4
+ // does not rely on agent-internal sandboxing for enforcement.
5
+ const AGENT_INTERNAL_SANDBOX_ENV_EXCLUSIONS = [
6
+ "GEMINI_SANDBOX",
7
+ "SEATBELT_PROFILE",
8
+ ];
2
9
  function isAxCredentialEnvironment(key) {
3
10
  return key.startsWith("AX_") && key.endsWith("_CREDENTIALS");
4
11
  }
5
12
  function buildBaseEnvironment(additionalExclusions = []) {
6
13
  const allExclusions = new Set([
7
14
  ...VAULT_ENV_EXCLUSIONS,
15
+ ...AGENT_INTERNAL_SANDBOX_ENV_EXCLUSIONS,
8
16
  ...additionalExclusions,
9
17
  ]);
10
18
  const entries = Object.entries(process.env).flatMap(([key, value]) => {
@@ -1,6 +1,5 @@
1
- import type { AgentCli } from "axshared";
1
+ import type { AgentCli, Credentials } from "axshared";
2
2
  import type { RunAgentOptions, ExecutionMetadata } from "./types/run-result.js";
3
- import type { Credentials } from "./credentials/credentials.js";
4
3
  import type { installCredentials } from "./credentials/install-credentials.js";
5
4
  type CredentialInstallResult = Awaited<ReturnType<typeof installCredentials>>;
6
5
  /**
@@ -19,7 +19,7 @@ function buildExecutionMetadata(agentId, options, credentialResult, credentials,
19
19
  if (credentials) {
20
20
  execution.credentials = {
21
21
  type: credentials.type,
22
- provider: credentials.provider,
22
+ provider: credentials.agent === "opencode" ? credentials.provider : undefined,
23
23
  };
24
24
  }
25
25
  return execution;
package/dist/cli.d.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * axexec CLI - Unified agent execution with isolation.
3
+ * axexec CLI - Unified agent execution with credential/config isolation.
4
4
  *
5
- * Executes AI coding agents with complete environment isolation:
6
- * - Credentials written to temp directory
7
- * - Config/permissions written to temp directory
8
- * - Agent pointed at temp directory via env vars
9
- * - Normalized event stream output
5
+ * axexec is a headless runner and normalization layer. It isolates agent
6
+ * credentials/config from your local environment via temp directories, but it
7
+ * is not an OS sandbox and is intentionally permissive/non-interactive by
8
+ * default. Use an external sandbox (container/VM) for real enforcement.
10
9
  */
11
10
  import "./agents/claude-code/adapter.js";
12
11
  import "./agents/codex/adapter.js";
package/dist/cli.js CHANGED
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * axexec CLI - Unified agent execution with isolation.
3
+ * axexec CLI - Unified agent execution with credential/config isolation.
4
4
  *
5
- * Executes AI coding agents with complete environment isolation:
6
- * - Credentials written to temp directory
7
- * - Config/permissions written to temp directory
8
- * - Agent pointed at temp directory via env vars
9
- * - Normalized event stream output
5
+ * axexec is a headless runner and normalization layer. It isolates agent
6
+ * credentials/config from your local environment via temp directories, but it
7
+ * is not an OS sandbox and is intentionally permissive/non-interactive by
8
+ * default. Use an external sandbox (container/VM) for real enforcement.
10
9
  */
11
10
  import { Command } from "@commander-js/extra-typings";
12
11
  import packageJson from "../package.json" with { type: "json" };
@@ -17,14 +16,15 @@ import "./agents/gemini/adapter.js";
17
16
  import "./agents/opencode/adapter.js";
18
17
  import "./agents/copilot/adapter.js";
19
18
  import { listAdapters } from "./agents/registry.js";
19
+ import { readStdin } from "./read-stdin.js";
20
+ import { resolveCredentials } from "./resolve-credentials.js";
21
+ import { parseOutputFormat } from "./resolve-output-mode.js";
22
+ import { resolvePrompt } from "./resolve-prompt.js";
20
23
  import { runAgent } from "./run-agent.js";
21
- async function readStdin() {
22
- let data = "";
23
- for await (const chunk of process.stdin) {
24
- data += typeof chunk === "string" ? chunk : chunk.toString("utf8");
25
- }
26
- return data.trimEnd();
27
- }
24
+ import { validateAgent } from "./validate-agent.js";
25
+ import { validateCwd } from "./validate-cwd.js";
26
+ import { validateProviderOptions } from "./validate-opencode-options.js";
27
+ import { validateStdinUsage } from "./validate-stdin-usage.js";
28
28
  const program = new Command()
29
29
  .name(packageJson.name)
30
30
  .description(packageJson.description)
@@ -33,11 +33,13 @@ const program = new Command()
33
33
  .showSuggestionAfterError()
34
34
  .option("--list-agents", "List available agents")
35
35
  .option("-a, --agent <id>", "Agent to use (required)")
36
+ .option("--cwd <path>", "Working directory for the agent process")
37
+ .option("--credentials-file <path|->", "Read credentials JSON from file (or '-' for stdin)")
36
38
  .option("-p, --prompt <text>", "Prompt text")
37
39
  .option("-m, --model <model>", "Model to use")
38
40
  .option("--provider <provider>", "Provider for OpenCode (required for OpenCode)")
39
- .option("--allow <perms>", "Permission rules to allow (comma-separated)")
40
- .option("--deny <perms>", "Permission rules to deny (comma-separated)")
41
+ .option("--allow <perms>", "Allow permissions (best-effort, comma-separated)")
42
+ .option("--deny <perms>", "Deny permissions (best-effort, comma-separated)")
41
43
  .option("-f, --format <fmt>", "Output format: jsonl, tsv (default: tsv, truncated on TTY)")
42
44
  .option("--raw-log <file>", "Write raw agent output to file")
43
45
  .option("--debug", "Enable debug mode")
@@ -48,9 +50,11 @@ const program = new Command()
48
50
  Requirements:
49
51
  Install the agent CLIs you plan to use: claude, codex, gemini, opencode, copilot.
50
52
  Override paths: AXEXEC_CLAUDE_PATH, AXEXEC_CODEX_PATH, AXEXEC_GEMINI_PATH, AXEXEC_OPENCODE_PATH, AXEXEC_COPILOT_PATH
53
+ Security: axexec is not a sandbox. Run inside an external sandbox for enforcement.
51
54
 
52
55
  Examples:
53
56
  axexec -a claude "Refactor auth flow"
57
+ axexec -a claude --credentials-file ./creds.json "Review this PR"
54
58
  axexec -a opencode --provider anthropic -m claude-sonnet-4 "Hello"
55
59
  axexec -a claude -f jsonl "Audit deps" | jq 'select(.type=="tool.call")'
56
60
  axexec --list-agents | tail -n +2 | cut -f1
@@ -65,71 +69,89 @@ Examples:
65
69
  }
66
70
  return;
67
71
  }
68
- // Get prompt from flag, positional argument, or stdin (in priority order)
69
- // Only read stdin if no prompt was provided via CLI arguments to avoid
70
- // blocking in CI environments with open but unused stdin.
71
- const needsStdinPrompt = !options.prompt && !positionalPrompt;
72
- const stdinPrompt = needsStdinPrompt && !process.stdin.isTTY ? await readStdin() : undefined;
73
- const prompt = (options.prompt ??
74
- positionalPrompt ??
75
- stdinPrompt)?.trimEnd();
76
- if (!prompt) {
77
- process.stderr.write("Error: prompt is required\n");
72
+ if (!options.agent) {
73
+ process.stderr.write("Error: --agent is required\n");
78
74
  process.stderr.write("Usage: axexec --agent <id> <prompt>\n");
79
75
  process.stderr.write("Try 'axexec --help' for more information.\n");
80
76
  process.exitCode = 2;
81
77
  return;
82
78
  }
83
- if (!options.agent) {
84
- process.stderr.write("Error: --agent is required\n");
85
- process.stderr.write("Usage: axexec --agent <id> <prompt>\n");
86
- process.stderr.write("Try 'axexec --help' for more information.\n");
79
+ // Validate agent ID early to provide clear error before processing credentials
80
+ const agentValidation = validateAgent(options.agent);
81
+ if (!agentValidation.ok) {
82
+ process.stderr.write(`${agentValidation.message}\n`);
83
+ process.exitCode = agentValidation.exitCode;
84
+ return;
85
+ }
86
+ // Validate --cwd if provided
87
+ if (options.cwd) {
88
+ const cwdResult = await validateCwd(options.cwd);
89
+ if (!cwdResult.ok) {
90
+ process.stderr.write(`Error: ${cwdResult.error}\n`);
91
+ process.exitCode = 2;
92
+ return;
93
+ }
94
+ }
95
+ const needsStdinPrompt = !options.prompt && !positionalPrompt;
96
+ const stdinResult = validateStdinUsage(options.credentialsFile, needsStdinPrompt, process.stdin.isTTY);
97
+ if (!stdinResult.ok) {
98
+ process.stderr.write(`Error: ${stdinResult.error}\n`);
87
99
  process.exitCode = 2;
88
100
  return;
89
101
  }
102
+ // Get prompt from flag, positional argument, or stdin (in priority order)
103
+ // Only read stdin if no prompt was provided via CLI arguments to avoid
104
+ // blocking in CI environments with open but unused stdin.
105
+ const stdinPrompt = needsStdinPrompt &&
106
+ !process.stdin.isTTY &&
107
+ options.credentialsFile !== "-"
108
+ ? await readStdin()
109
+ : undefined;
110
+ const promptResult = resolvePrompt({
111
+ promptFlag: options.prompt,
112
+ positionalPrompt,
113
+ stdinPrompt,
114
+ });
115
+ if (!promptResult.ok) {
116
+ process.stderr.write(`Error: ${promptResult.error}\n`);
117
+ process.exitCode = 2;
118
+ return;
119
+ }
120
+ const { prompt } = promptResult;
90
121
  // Validate format option
91
122
  let format;
92
123
  if (options.format) {
93
- if (options.format === "jsonl" || options.format === "tsv") {
94
- format = options.format;
95
- }
96
- else {
97
- process.stderr.write(`Error: Invalid format '${options.format}'\n`);
98
- process.stderr.write("Valid formats: jsonl, tsv\n");
124
+ const formatResult = parseOutputFormat(options.format);
125
+ if (!formatResult.ok) {
126
+ process.stderr.write(`Error: ${formatResult.error}\n`);
99
127
  process.exitCode = 2;
100
128
  return;
101
129
  }
130
+ format = formatResult.format;
102
131
  }
103
- // Validate --provider is only used with OpenCode
104
- if (options.provider && options.agent !== "opencode") {
105
- process.stderr.write("Error: --provider is only supported for OpenCode agent\n");
132
+ // Resolve credentials from --credentials-file if specified.
133
+ const credentialsResult = await resolveCredentials(options.credentialsFile, readStdin, options.agent);
134
+ if (!credentialsResult.ok) {
135
+ process.stderr.write(`Error: ${credentialsResult.error}\n`);
106
136
  process.exitCode = 2;
107
137
  return;
108
138
  }
109
- // Validate OpenCode requires --provider
110
- if (options.agent === "opencode") {
111
- // Check for deprecated provider/model format
112
- if (options.model?.includes("/")) {
113
- const [provider, model] = options.model.split("/", 2);
114
- process.stderr.write("Error: Model format 'provider/model' is no longer supported\n");
115
- process.stderr.write("Use separate --provider and --model flags:\n");
116
- process.stderr.write(` axexec -a opencode --provider ${provider} -m ${model} ...\n`);
117
- process.exitCode = 2;
118
- return;
119
- }
120
- // Require --provider for OpenCode
121
- if (!options.provider) {
122
- process.stderr.write("Error: OpenCode requires --provider\n");
123
- process.stderr.write(" axexec -a opencode --provider anthropic -m claude-sonnet-4 ...\n");
124
- process.exitCode = 2;
125
- return;
126
- }
139
+ const { credentials } = credentialsResult;
140
+ // Validate provider options
141
+ const providerResult = validateProviderOptions(options.agent, options.model, options.provider, credentials);
142
+ if (!providerResult.ok) {
143
+ process.stderr.write(`Error: ${providerResult.error}\n`);
144
+ process.exitCode = 2;
145
+ return;
127
146
  }
147
+ const { normalizedProvider } = providerResult;
128
148
  // Run agent
129
149
  const result = await runAgent(options.agent, {
130
150
  prompt,
151
+ cwd: options.cwd,
152
+ credentials,
131
153
  model: options.model,
132
- provider: options.provider,
154
+ provider: normalizedProvider,
133
155
  allow: options.allow,
134
156
  deny: options.deny,
135
157
  format,
@@ -138,9 +160,8 @@ Examples:
138
160
  verbose: options.verbose,
139
161
  preserveGithubSha: options.preserveGithubSha,
140
162
  });
141
- // Set exit code based on success
142
- if (!result.success) {
143
- // eslint-disable-next-line require-atomic-updates
163
+ // Set exit code based on success (only if not already set to a more specific code)
164
+ if (!result.success && process.exitCode === undefined) {
144
165
  process.exitCode = 1;
145
166
  }
146
167
  });
@@ -4,7 +4,7 @@
4
4
  * Each agent has its own env var requirements. This module extracts
5
5
  * the appropriate values from Credentials and returns them as env vars.
6
6
  */
7
- import type { Credentials } from "./credentials.js";
7
+ import type { Credentials } from "axshared";
8
8
  /**
9
9
  * Gets additional environment variables for credential-based auth.
10
10
  *
@@ -51,7 +51,7 @@ function getCredentialEnvironment(credentials) {
51
51
  }
52
52
  case "opencode": {
53
53
  // OpenCode supports multiple providers, env var depends on provider
54
- const provider = credentials.provider ?? "anthropic";
54
+ const provider = credentials.provider;
55
55
  if (credentials.type === "api-key" && apiKey) {
56
56
  switch (provider) {
57
57
  case "anthropic": {
@@ -3,11 +3,10 @@
3
3
  *
4
4
  * Writes credentials to a temporary directory in agent-specific formats,
5
5
  * then returns the environment variables needed to point the agent at that
6
- * directory. This ensures complete isolation - agents never discover or use
7
- * locally installed credentials.
6
+ * directory. This ensures credential isolation: agents don't discover or use
7
+ * locally installed credentials/config.
8
8
  */
9
- import { type AgentCli } from "axshared";
10
- import type { Credentials } from "./credentials.js";
9
+ import { type AgentCli, type Credentials } from "axshared";
11
10
  import type { InstallResult } from "./types.js";
12
11
  type WarningWriter = (message: string) => void;
13
12
  /**
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Writes credentials to a temporary directory in agent-specific formats,
5
5
  * then returns the environment variables needed to point the agent at that
6
- * directory. This ensures complete isolation - agents never discover or use
7
- * locally installed credentials.
6
+ * directory. This ensures credential isolation: agents don't discover or use
7
+ * locally installed credentials/config.
8
8
  */
9
9
  import { mkdirSync } from "node:fs";
10
10
  import { mkdtemp, rm } from "node:fs/promises";
@@ -57,7 +57,9 @@ function installAgentCredentials(agentId, configDirectory, dataDirectory, creden
57
57
  break;
58
58
  }
59
59
  case "opencode": {
60
- installOpenCodeCredentials(dataDirectory, credentials, warn);
60
+ if (credentials.agent === "opencode") {
61
+ installOpenCodeCredentials(dataDirectory, credentials, warn);
62
+ }
61
63
  break;
62
64
  }
63
65
  case "copilot": {
@@ -5,7 +5,7 @@
5
5
  * write credentials in the agent-specific format, extracting what
6
6
  * they need from the canonical Credentials type.
7
7
  */
8
- import type { Credentials } from "./credentials.js";
8
+ import type { Credentials } from "axshared";
9
9
  type WarningWriter = (message: string) => void;
10
10
  /**
11
11
  * Installs Claude credentials to a config directory.
@@ -54,5 +54,7 @@ declare function installGeminiCredentials(configDirectory: string, credentials:
54
54
  * - oauth-credentials: Written as { [provider]: { type: "oauth", ...data } }
55
55
  * - api-key: Written as { [provider]: { type: "api", key: "..." } }
56
56
  */
57
- declare function installOpenCodeCredentials(dataDirectory: string, credentials: Credentials, warn?: WarningWriter): void;
57
+ declare function installOpenCodeCredentials(dataDirectory: string, credentials: Extract<Credentials, {
58
+ agent: "opencode";
59
+ }>, warn?: WarningWriter): void;
58
60
  export { installClaudeCredentials, installCodexCredentials, installGeminiCredentials, installOpenCodeCredentials, };