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.
- package/README.md +32 -22
- package/dist/agents/copilot/stream-session.js +8 -7
- package/dist/agents/copilot/watch-session.js +3 -1
- package/dist/agents/opencode/adapter.js +8 -1
- package/dist/agents/opencode/build-permission-environment.d.ts +17 -0
- package/dist/agents/opencode/build-permission-environment.js +31 -0
- package/dist/agents/opencode/parse-sse-event.js +3 -1
- package/dist/agents/opencode/spawn-server.js +12 -1
- package/dist/build-agent-environment.js +8 -0
- package/dist/build-execution-metadata.d.ts +1 -2
- package/dist/build-execution-metadata.js +1 -1
- package/dist/cli.d.ts +5 -6
- package/dist/cli.js +81 -60
- package/dist/credentials/get-credential-environment.d.ts +1 -1
- package/dist/credentials/get-credential-environment.js +1 -1
- package/dist/credentials/install-credentials.d.ts +3 -4
- package/dist/credentials/install-credentials.js +5 -3
- package/dist/credentials/write-agent-credentials.d.ts +4 -2
- package/dist/credentials/write-agent-credentials.js +1 -6
- package/dist/execute-agent.d.ts +1 -2
- package/dist/execute-agent.js +1 -0
- package/dist/format-zod-error.d.ts +6 -0
- package/dist/format-zod-error.js +12 -0
- package/dist/index.d.ts +1 -1
- package/dist/parse-credentials.d.ts +1 -2
- package/dist/parse-credentials.js +7 -6
- package/dist/read-credentials-file.d.ts +13 -0
- package/dist/read-credentials-file.js +39 -0
- package/dist/read-stdin.d.ts +5 -0
- package/dist/read-stdin.js +11 -0
- package/dist/resolve-credentials.d.ts +21 -0
- package/dist/resolve-credentials.js +23 -0
- package/dist/resolve-output-mode.d.ts +15 -1
- package/dist/resolve-output-mode.js +16 -1
- package/dist/resolve-prompt.d.ts +22 -0
- package/dist/resolve-prompt.js +23 -0
- package/dist/run-agent.d.ts +4 -1
- package/dist/run-agent.js +47 -6
- package/dist/types/run-result.d.ts +5 -2
- package/dist/validate-cwd.d.ts +11 -0
- package/dist/validate-cwd.js +24 -0
- package/dist/validate-opencode-options.d.ts +19 -0
- package/dist/validate-opencode-options.js +58 -0
- package/dist/validate-stdin-usage.d.ts +18 -0
- package/dist/validate-stdin-usage.js +28 -0
- package/package.json +1 -1
- package/dist/credentials/credentials.d.ts +0 -39
- 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
|
|
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
|
-
- **
|
|
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>
|
|
93
|
-
--deny <perms>
|
|
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 |
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
27
|
-
//
|
|
28
|
-
|
|
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 =
|
|
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
|
|
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}.
|
|
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: { ...
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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>", "
|
|
40
|
-
.option("--deny <perms>", "
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
process.stderr.write(
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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:
|
|
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 "
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 "
|
|
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,
|
|
57
|
+
declare function installOpenCodeCredentials(dataDirectory: string, credentials: Extract<Credentials, {
|
|
58
|
+
agent: "opencode";
|
|
59
|
+
}>, warn?: WarningWriter): void;
|
|
58
60
|
export { installClaudeCredentials, installCodexCredentials, installGeminiCredentials, installOpenCodeCredentials, };
|