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.
- package/README.md +32 -22
- package/dist/agents/claude-code/types.d.ts +2 -2
- package/dist/agents/copilot/stream-session.js +8 -7
- package/dist/agents/copilot/watch-session.js +3 -1
- package/dist/agents/gemini/types.d.ts +4 -4
- 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/cli.d.ts +5 -6
- package/dist/cli.js +81 -60
- package/dist/credentials/install-credentials.d.ts +2 -2
- package/dist/credentials/install-credentials.js +2 -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/parse-credentials.js +1 -8
- 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 +4 -0
- 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/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
|
|
@@ -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
|
-
"
|
|
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
|
/**
|
|
@@ -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
|
|
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]) => {
|
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
|
});
|
|
@@ -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 { 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
|
|
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";
|
package/dist/execute-agent.js
CHANGED
|
@@ -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,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
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/run-agent.d.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { RunAgentOptions, RunResult } from "./types/run-result.js";
|
|
5
5
|
/**
|
|
6
|
-
* Runs an agent with
|
|
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
|
|
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
|
|
39
|
-
if (
|
|
40
|
-
const
|
|
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 ${
|
|
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,
|
|
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,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