axexec 1.6.1 → 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 (39) hide show
  1. package/README.md +32 -22
  2. package/dist/agents/claude-code/types.d.ts +2 -2
  3. package/dist/agents/copilot/stream-session.js +8 -7
  4. package/dist/agents/copilot/watch-session.js +3 -1
  5. package/dist/agents/gemini/types.d.ts +4 -4
  6. package/dist/agents/opencode/adapter.js +8 -1
  7. package/dist/agents/opencode/build-permission-environment.d.ts +17 -0
  8. package/dist/agents/opencode/build-permission-environment.js +31 -0
  9. package/dist/agents/opencode/parse-sse-event.js +3 -1
  10. package/dist/agents/opencode/spawn-server.js +12 -1
  11. package/dist/build-agent-environment.js +8 -0
  12. package/dist/cli.d.ts +5 -6
  13. package/dist/cli.js +81 -60
  14. package/dist/credentials/install-credentials.d.ts +2 -2
  15. package/dist/credentials/install-credentials.js +2 -2
  16. package/dist/execute-agent.js +1 -0
  17. package/dist/format-zod-error.d.ts +6 -0
  18. package/dist/format-zod-error.js +12 -0
  19. package/dist/parse-credentials.js +1 -8
  20. package/dist/read-credentials-file.d.ts +13 -0
  21. package/dist/read-credentials-file.js +39 -0
  22. package/dist/read-stdin.d.ts +5 -0
  23. package/dist/read-stdin.js +11 -0
  24. package/dist/resolve-credentials.d.ts +21 -0
  25. package/dist/resolve-credentials.js +23 -0
  26. package/dist/resolve-output-mode.d.ts +15 -1
  27. package/dist/resolve-output-mode.js +16 -1
  28. package/dist/resolve-prompt.d.ts +22 -0
  29. package/dist/resolve-prompt.js +23 -0
  30. package/dist/run-agent.d.ts +4 -1
  31. package/dist/run-agent.js +47 -6
  32. package/dist/types/run-result.d.ts +4 -0
  33. package/dist/validate-cwd.d.ts +11 -0
  34. package/dist/validate-cwd.js +24 -0
  35. package/dist/validate-opencode-options.d.ts +19 -0
  36. package/dist/validate-opencode-options.js +58 -0
  37. package/dist/validate-stdin-usage.d.ts +18 -0
  38. package/dist/validate-stdin-usage.js +28 -0
  39. package/package.json +1 -1
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
@@ -77,8 +77,8 @@ type ClaudeUserEvent = z.infer<typeof ClaudeUserEvent>;
77
77
  declare const ClaudeResultEvent: z.ZodObject<{
78
78
  type: z.ZodLiteral<"result">;
79
79
  subtype: z.ZodEnum<{
80
- success: "success";
81
80
  error: "error";
81
+ success: "success";
82
82
  cancelled: "cancelled";
83
83
  }>;
84
84
  timestamp: z.ZodOptional<z.ZodString>;
@@ -153,8 +153,8 @@ declare const ClaudeEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
153
153
  }, z.core.$strip>, z.ZodObject<{
154
154
  type: z.ZodLiteral<"result">;
155
155
  subtype: z.ZodEnum<{
156
- success: "success";
157
156
  error: "error";
157
+ success: "success";
158
158
  cancelled: "cancelled";
159
159
  }>;
160
160
  timestamp: z.ZodOptional<z.ZodString>;
@@ -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
  /**
@@ -41,8 +41,8 @@ declare const GeminiToolResultEvent: z.ZodObject<{
41
41
  type: z.ZodLiteral<"tool_result">;
42
42
  tool_id: z.ZodString;
43
43
  status: z.ZodEnum<{
44
- success: "success";
45
44
  error: "error";
45
+ success: "success";
46
46
  }>;
47
47
  output: z.ZodOptional<z.ZodString>;
48
48
  error: z.ZodOptional<z.ZodObject<{
@@ -67,8 +67,8 @@ type GeminiErrorEvent = z.infer<typeof GeminiErrorEvent>;
67
67
  declare const GeminiResultEvent: z.ZodObject<{
68
68
  type: z.ZodLiteral<"result">;
69
69
  status: z.ZodEnum<{
70
- success: "success";
71
70
  error: "error";
71
+ success: "success";
72
72
  }>;
73
73
  error: z.ZodOptional<z.ZodObject<{
74
74
  type: z.ZodString;
@@ -109,8 +109,8 @@ declare const GeminiEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
109
109
  type: z.ZodLiteral<"tool_result">;
110
110
  tool_id: z.ZodString;
111
111
  status: z.ZodEnum<{
112
- success: "success";
113
112
  error: "error";
113
+ success: "success";
114
114
  }>;
115
115
  output: z.ZodOptional<z.ZodString>;
116
116
  error: z.ZodOptional<z.ZodObject<{
@@ -129,8 +129,8 @@ declare const GeminiEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
129
129
  }, z.core.$strip>, z.ZodObject<{
130
130
  type: z.ZodLiteral<"result">;
131
131
  status: z.ZodEnum<{
132
- success: "success";
133
132
  error: "error";
133
+ success: "success";
134
134
  }>;
135
135
  error: z.ZodOptional<z.ZodObject<{
136
136
  type: z.ZodString;
@@ -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]) => {
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
  });
@@ -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 { type AgentCli, type Credentials } from "axshared";
10
10
  import type { InstallResult } from "./types.js";
@@ -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";
@@ -12,6 +12,7 @@ async function executeAgent(agentId, adapter, options, configEnvironment, creden
12
12
  const runOptions = {
13
13
  prompt: options.prompt,
14
14
  verbose: options.verbose ?? false,
15
+ cwd: options.cwd,
15
16
  model: options.model,
16
17
  provider: options.provider,
17
18
  rawLogPath: options.rawLog,
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Format Zod validation errors for human-readable display.
3
+ */
4
+ import type { ZodError } from "zod";
5
+ declare function formatZodError(error: ZodError): string;
6
+ export { formatZodError };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Format Zod validation errors for human-readable display.
3
+ */
4
+ function formatZodError(error) {
5
+ return error.issues
6
+ .map((issue) => {
7
+ const path = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
8
+ return `${path}: ${issue.message}`;
9
+ })
10
+ .join("; ");
11
+ }
12
+ export { formatZodError };
@@ -6,14 +6,7 @@
6
6
  */
7
7
  import { parseCredentialsResult, } from "axshared";
8
8
  import { getEnvironmentTrimmed } from "./credentials/get-environment-trimmed.js";
9
- function formatZodError(error) {
10
- return error.issues
11
- .map((issue) => {
12
- const path = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
13
- return `${path}: ${issue.message}`;
14
- })
15
- .join("; ");
16
- }
9
+ import { formatZodError } from "./format-zod-error.js";
17
10
  /**
18
11
  * Parses credentials from environment variables.
19
12
  *
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Read and parse credentials from a file or stdin.
3
+ */
4
+ import { type Credentials } from "axshared";
5
+ type ReadCredentialsResult = {
6
+ ok: true;
7
+ credentials: Credentials;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ };
12
+ declare function readCredentialsFile(path: string, readStdin: () => Promise<string>, expectedAgent: string): Promise<ReadCredentialsResult>;
13
+ export { readCredentialsFile };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Read and parse credentials from a file or stdin.
3
+ */
4
+ import { readFile } from "node:fs/promises";
5
+ import { parseCredentialsResult } from "axshared";
6
+ import { formatZodError } from "./format-zod-error.js";
7
+ async function readCredentialsFile(path, readStdin, expectedAgent) {
8
+ let raw;
9
+ try {
10
+ raw = path === "-" ? await readStdin() : await readFile(path, "utf8");
11
+ }
12
+ catch (error) {
13
+ const message = error instanceof Error ? error.message : String(error);
14
+ return { ok: false, error: `Failed to read credentials file: ${message}` };
15
+ }
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(raw);
19
+ }
20
+ catch (error) {
21
+ const message = error instanceof Error ? error.message : String(error);
22
+ return { ok: false, error: `Failed to parse credentials JSON: ${message}` };
23
+ }
24
+ const parseResult = parseCredentialsResult(parsed);
25
+ if (!parseResult.ok) {
26
+ return {
27
+ ok: false,
28
+ error: `Invalid credentials format: ${formatZodError(parseResult.error)}`,
29
+ };
30
+ }
31
+ if (parseResult.value.agent !== expectedAgent) {
32
+ return {
33
+ ok: false,
34
+ error: `Credentials agent mismatch. Expected ${expectedAgent}, got ${parseResult.value.agent}.`,
35
+ };
36
+ }
37
+ return { ok: true, credentials: parseResult.value };
38
+ }
39
+ export { readCredentialsFile };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Read all data from stdin as a string.
3
+ */
4
+ declare function readStdin(): Promise<string>;
5
+ export { readStdin };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Read all data from stdin as a string.
3
+ */
4
+ async function readStdin() {
5
+ let data = "";
6
+ for await (const chunk of process.stdin) {
7
+ data += typeof chunk === "string" ? chunk : chunk.toString("utf8");
8
+ }
9
+ return data.trimEnd();
10
+ }
11
+ export { readStdin };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Resolve credentials from --credentials-file option.
3
+ */
4
+ import type { Credentials } from "axshared";
5
+ type ResolveCredentialsResult = {
6
+ ok: true;
7
+ credentials: Credentials | undefined;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ };
12
+ /**
13
+ * Resolve credentials from --credentials-file if specified.
14
+ *
15
+ * @param credentialsFile - Path to credentials file or "-" for stdin
16
+ * @param readStdin - Function to read stdin
17
+ * @param agentId - Expected agent ID for validation
18
+ * @returns Credentials or undefined if no file specified
19
+ */
20
+ declare function resolveCredentials(credentialsFile: string | undefined, readStdin: () => Promise<string>, agentId: string): Promise<ResolveCredentialsResult>;
21
+ export { resolveCredentials };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Resolve credentials from --credentials-file option.
3
+ */
4
+ import { readCredentialsFile } from "./read-credentials-file.js";
5
+ /**
6
+ * Resolve credentials from --credentials-file if specified.
7
+ *
8
+ * @param credentialsFile - Path to credentials file or "-" for stdin
9
+ * @param readStdin - Function to read stdin
10
+ * @param agentId - Expected agent ID for validation
11
+ * @returns Credentials or undefined if no file specified
12
+ */
13
+ async function resolveCredentials(credentialsFile, readStdin, agentId) {
14
+ if (!credentialsFile) {
15
+ return { ok: true, credentials: undefined };
16
+ }
17
+ const result = await readCredentialsFile(credentialsFile, readStdin, agentId);
18
+ if (!result.ok) {
19
+ return { ok: false, error: result.error };
20
+ }
21
+ return { ok: true, credentials: result.credentials };
22
+ }
23
+ export { resolveCredentials };
@@ -35,5 +35,19 @@ type OutputMode = "jsonl" | "tsv" | "tsv-truncated";
35
35
  * resolveOutputMode(undefined, false) // → "tsv"
36
36
  */
37
37
  declare function resolveOutputMode(format: OutputFormat | undefined, isTTY: boolean): OutputMode;
38
- export { resolveOutputMode };
38
+ type ParseFormatResult = {
39
+ ok: true;
40
+ format: OutputFormat;
41
+ } | {
42
+ ok: false;
43
+ error: string;
44
+ };
45
+ /**
46
+ * Parse and validate a format string from CLI input.
47
+ *
48
+ * @param input - Raw format string from --format flag
49
+ * @returns Result with validated format or error message
50
+ */
51
+ declare function parseOutputFormat(input: string): ParseFormatResult;
52
+ export { resolveOutputMode, parseOutputFormat };
39
53
  export type { OutputFormat, OutputMode };
@@ -36,4 +36,19 @@ function resolveOutputMode(format, isTTY) {
36
36
  // Auto-detect based on TTY
37
37
  return isTTY ? "tsv-truncated" : "tsv";
38
38
  }
39
- export { resolveOutputMode };
39
+ /**
40
+ * Parse and validate a format string from CLI input.
41
+ *
42
+ * @param input - Raw format string from --format flag
43
+ * @returns Result with validated format or error message
44
+ */
45
+ function parseOutputFormat(input) {
46
+ if (input === "jsonl" || input === "tsv") {
47
+ return { ok: true, format: input };
48
+ }
49
+ return {
50
+ ok: false,
51
+ error: `Invalid format '${input}'\nValid formats: jsonl, tsv`,
52
+ };
53
+ }
54
+ export { resolveOutputMode, parseOutputFormat };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Resolve prompt from CLI options, positional argument, or stdin.
3
+ */
4
+ type ResolvePromptResult = {
5
+ ok: true;
6
+ prompt: string;
7
+ } | {
8
+ ok: false;
9
+ error: string;
10
+ };
11
+ interface ResolvePromptOptions {
12
+ promptFlag: string | undefined;
13
+ positionalPrompt: string | undefined;
14
+ stdinPrompt: string | undefined;
15
+ }
16
+ /**
17
+ * Resolve prompt from multiple sources in priority order.
18
+ *
19
+ * Priority: --prompt flag > positional argument > stdin
20
+ */
21
+ declare function resolvePrompt(options: ResolvePromptOptions): ResolvePromptResult;
22
+ export { resolvePrompt };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Resolve prompt from CLI options, positional argument, or stdin.
3
+ */
4
+ /**
5
+ * Resolve prompt from multiple sources in priority order.
6
+ *
7
+ * Priority: --prompt flag > positional argument > stdin
8
+ */
9
+ function resolvePrompt(options) {
10
+ const prompt = (options.promptFlag ??
11
+ options.positionalPrompt ??
12
+ options.stdinPrompt)?.trimEnd();
13
+ if (!prompt) {
14
+ return {
15
+ ok: false,
16
+ error: "prompt is required\n" +
17
+ "Usage: axexec --agent <id> <prompt>\n" +
18
+ "Try 'axexec --help' for more information.",
19
+ };
20
+ }
21
+ return { ok: true, prompt };
22
+ }
23
+ export { resolvePrompt };
@@ -3,7 +3,10 @@
3
3
  */
4
4
  import type { RunAgentOptions, RunResult } from "./types/run-result.js";
5
5
  /**
6
- * Runs an agent with full isolation.
6
+ * Runs an agent with credential/config isolation.
7
+ *
8
+ * Note: axexec is not an OS sandbox. It runs agents in a non-interactive,
9
+ * permissive mode by default and should not be treated as a security boundary.
7
10
  *
8
11
  * 1. Validates the agent ID
9
12
  * 2. Uses provided credentials or parses from environment
package/dist/run-agent.js CHANGED
@@ -4,12 +4,16 @@
4
4
  import { installCredentials, cleanupCredentials, } from "./credentials/install-credentials.js";
5
5
  import { buildPermissionsConfig } from "./build-permissions-config.js";
6
6
  import { validateAgent } from "./validate-agent.js";
7
+ import { validateCwd } from "./validate-cwd.js";
7
8
  import { parseCredentialsFromEnvironment } from "./parse-credentials.js";
8
9
  import { buildExecutionMetadata } from "./build-execution-metadata.js";
9
10
  import { executeAgent } from "./execute-agent.js";
10
11
  import { resolveDiagnostics, resolveExitCodeSetter, } from "./resolve-run-diagnostics.js";
11
12
  /**
12
- * Runs an agent with full isolation.
13
+ * Runs an agent with credential/config isolation.
14
+ *
15
+ * Note: axexec is not an OS sandbox. It runs agents in a non-interactive,
16
+ * permissive mode by default and should not be treated as a security boundary.
13
17
  *
14
18
  * 1. Validates the agent ID
15
19
  * 2. Uses provided credentials or parses from environment
@@ -35,12 +39,28 @@ async function runAgent(agentId, options) {
35
39
  };
36
40
  }
37
41
  const { adapter } = validation;
38
- // Validate --model format for OpenCode (reject "provider/model" format)
39
- if (validation.agentId === "opencode" && options.model?.includes("/")) {
40
- const [provider, model] = options.model.split("/", 2);
42
+ // Validate cwd if provided (for programmatic API consumers)
43
+ if (options.cwd) {
44
+ const cwdValidation = await validateCwd(options.cwd);
45
+ if (!cwdValidation.ok) {
46
+ diagnostics.error(`Error: ${cwdValidation.error}`);
47
+ setExitCode(2);
48
+ return {
49
+ events: [],
50
+ success: false,
51
+ execution: { agent: validation.agentId, model: options.model },
52
+ };
53
+ }
54
+ }
55
+ // Validate --model format for OpenCode (reject "provider/model" format only when --provider is missing).
56
+ // Many legitimate model IDs contain slashes (e.g., nvidia/nemotron, google/gemma).
57
+ if (validation.agentId === "opencode" &&
58
+ !options.provider &&
59
+ options.model?.includes("/")) {
60
+ const [providerPart, modelPart] = options.model.split("/", 2);
41
61
  diagnostics.error(`Error: Model format 'provider/model' is no longer supported.\n` +
42
62
  `Use separate --provider and --model flags instead:\n` +
43
- ` --provider ${provider} --model ${model}`);
63
+ ` --provider ${providerPart} --model ${modelPart}`);
44
64
  setExitCode(2);
45
65
  return {
46
66
  events: [],
@@ -73,6 +93,27 @@ async function runAgent(agentId, options) {
73
93
  execution: { agent: validation.agentId, model: options.model },
74
94
  };
75
95
  }
96
+ // Validate OpenCode credentials provider matches the requested provider.
97
+ // This duplicates validation in validateOpenCodeOptions() (CLI layer) but is
98
+ // necessary for programmatic API consumers who call runAgent() directly.
99
+ if (validation.agentId === "opencode" &&
100
+ credentials?.agent === "opencode" &&
101
+ options.provider &&
102
+ credentials.provider.toLowerCase() !== options.provider.toLowerCase()) {
103
+ diagnostics.error("Error: Credentials provider mismatch. " +
104
+ `Expected ${options.provider}, got ${credentials.provider}.`);
105
+ setExitCode(2);
106
+ return {
107
+ events: [],
108
+ success: false,
109
+ execution: { agent: validation.agentId, model: options.model },
110
+ };
111
+ }
112
+ // Normalize provider to lowercase for OpenCode (all OpenCode provider IDs are lowercase).
113
+ // This ensures consistent behavior for both CLI and programmatic API users.
114
+ const normalizedOptions = validation.agentId === "opencode" && options.provider
115
+ ? { ...options, provider: options.provider.toLowerCase() }
116
+ : options;
76
117
  // Always create isolated runtime directories; install credentials if provided.
77
118
  let credentialResult;
78
119
  try {
@@ -112,6 +153,6 @@ async function runAgent(agentId, options) {
112
153
  configEnvironment = configResult.env;
113
154
  }
114
155
  // Execute agent
115
- return executeAgent(validation.agentId, adapter, options, configEnvironment, credentialResult, credentials, preserveConfigDirectory, diagnostics.error);
156
+ return executeAgent(validation.agentId, adapter, normalizedOptions, configEnvironment, credentialResult, credentials, preserveConfigDirectory, diagnostics.error);
116
157
  }
117
158
  export { runAgent };
@@ -10,6 +10,10 @@ interface RunAgentDiagnostics {
10
10
  }
11
11
  interface RunAgentOptions {
12
12
  prompt: string;
13
+ /**
14
+ * Working directory for the agent process.
15
+ */
16
+ cwd?: string;
13
17
  /**
14
18
  * Credentials to use. If not provided, parses from environment variables.
15
19
  * Accepts the canonical Credentials shape shared across ax* packages.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Validate --cwd option points to an existing directory.
3
+ */
4
+ type ValidateCwdResult = {
5
+ ok: true;
6
+ } | {
7
+ ok: false;
8
+ error: string;
9
+ };
10
+ declare function validateCwd(cwd: string): Promise<ValidateCwdResult>;
11
+ export { validateCwd };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Validate --cwd option points to an existing directory.
3
+ */
4
+ import { stat } from "node:fs/promises";
5
+ async function validateCwd(cwd) {
6
+ try {
7
+ const stats = await stat(cwd);
8
+ if (!stats.isDirectory()) {
9
+ return {
10
+ ok: false,
11
+ error: `--cwd path is not a directory: ${cwd}`,
12
+ };
13
+ }
14
+ return { ok: true };
15
+ }
16
+ catch (error) {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ return {
19
+ ok: false,
20
+ error: `--cwd directory error: ${message}`,
21
+ };
22
+ }
23
+ }
24
+ export { validateCwd };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Validate OpenCode-specific CLI options.
3
+ */
4
+ import type { Credentials } from "axshared";
5
+ type ProviderValidationResult = {
6
+ ok: true;
7
+ normalizedProvider: string | undefined;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ };
12
+ /**
13
+ * Validate provider usage and OpenCode-specific options.
14
+ *
15
+ * - For non-OpenCode agents: provider must not be specified
16
+ * - For OpenCode: validates model format, requires provider, checks credentials
17
+ */
18
+ declare function validateProviderOptions(agentId: string, model: string | undefined, provider: string | undefined, credentials: Credentials | undefined): ProviderValidationResult;
19
+ export { validateProviderOptions };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Validate OpenCode-specific CLI options.
3
+ */
4
+ /**
5
+ * Validate provider usage and OpenCode-specific options.
6
+ *
7
+ * - For non-OpenCode agents: provider must not be specified
8
+ * - For OpenCode: validates model format, requires provider, checks credentials
9
+ */
10
+ function validateProviderOptions(agentId, model, provider, credentials) {
11
+ // --provider is only valid for OpenCode
12
+ if (provider && agentId !== "opencode") {
13
+ return {
14
+ ok: false,
15
+ error: "--provider is only supported for OpenCode agent",
16
+ };
17
+ }
18
+ // Non-OpenCode agents don't need further validation
19
+ if (agentId !== "opencode") {
20
+ return { ok: true, normalizedProvider: undefined };
21
+ }
22
+ // OpenCode-specific validation
23
+ return validateOpenCodeOptions(model, provider, credentials);
24
+ }
25
+ function validateOpenCodeOptions(model, provider, credentials) {
26
+ // Require --provider for OpenCode
27
+ if (!provider) {
28
+ // If model contains "/" and no provider given, suggest they might be using
29
+ // the old provider/model format, but primarily tell them --provider is required.
30
+ if (model?.includes("/")) {
31
+ const [providerPart, modelPart] = model.split("/", 2);
32
+ return {
33
+ ok: false,
34
+ error: `OpenCode requires --provider\n` +
35
+ `If using the old 'provider/model' format, use separate flags:\n` +
36
+ ` axexec -a opencode --provider ${providerPart} -m ${modelPart} ...`,
37
+ };
38
+ }
39
+ return {
40
+ ok: false,
41
+ error: `OpenCode requires --provider\n` +
42
+ ` axexec -a opencode --provider anthropic -m claude-sonnet-4 ...`,
43
+ };
44
+ }
45
+ // Normalize to lowercase to match OpenCode's provider ID format.
46
+ // OpenCode stores all provider IDs in lowercase (anthropic, openai, etc).
47
+ const normalizedProvider = provider.toLowerCase();
48
+ // Validate credentials provider matches
49
+ if (credentials?.agent === "opencode" &&
50
+ credentials.provider.toLowerCase() !== normalizedProvider) {
51
+ return {
52
+ ok: false,
53
+ error: `Credentials provider mismatch. Expected ${normalizedProvider}, got ${credentials.provider}.`,
54
+ };
55
+ }
56
+ return { ok: true, normalizedProvider };
57
+ }
58
+ export { validateProviderOptions };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Validate stdin usage when credentials and prompt may both need stdin.
3
+ */
4
+ type ValidateStdinResult = {
5
+ ok: true;
6
+ } | {
7
+ ok: false;
8
+ error: string;
9
+ };
10
+ /**
11
+ * Validates that stdin usage is not conflicting.
12
+ *
13
+ * Returns an error if:
14
+ * - --credentials-file - is used with no prompt (both would read stdin)
15
+ * - --credentials-file - is used in interactive TTY mode (requires piping)
16
+ */
17
+ declare function validateStdinUsage(credentialsFile: string | undefined, needsStdinPrompt: boolean, isTTY: boolean | undefined): ValidateStdinResult;
18
+ export { validateStdinUsage };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Validate stdin usage when credentials and prompt may both need stdin.
3
+ */
4
+ /**
5
+ * Validates that stdin usage is not conflicting.
6
+ *
7
+ * Returns an error if:
8
+ * - --credentials-file - is used with no prompt (both would read stdin)
9
+ * - --credentials-file - is used in interactive TTY mode (requires piping)
10
+ */
11
+ function validateStdinUsage(credentialsFile, needsStdinPrompt, isTTY) {
12
+ if (credentialsFile === "-" && needsStdinPrompt) {
13
+ return {
14
+ ok: false,
15
+ error: "Cannot read both prompt and credentials from stdin\n" +
16
+ "Provide the prompt via --prompt or positional argument when using --credentials-file -",
17
+ };
18
+ }
19
+ if (credentialsFile === "-" && isTTY) {
20
+ return {
21
+ ok: false,
22
+ error: "Cannot read credentials from stdin in interactive mode\n" +
23
+ "Pipe credentials JSON to stdin or use a file path instead of '-'",
24
+ };
25
+ }
26
+ return { ok: true };
27
+ }
28
+ export { validateStdinUsage };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axexec",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "1.6.1",
5
+ "version": "1.7.0",
6
6
  "description": "Unified CLI runner for AI coding agents with normalized event streaming",
7
7
  "repository": {
8
8
  "type": "git",