axexec 1.0.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/LICENSE +21 -0
- package/README.md +196 -0
- package/bin/axexec +17 -0
- package/dist/agents/claude-code/adapter.d.ts +6 -0
- package/dist/agents/claude-code/adapter.js +71 -0
- package/dist/agents/claude-code/parse-event.d.ts +16 -0
- package/dist/agents/claude-code/parse-event.js +185 -0
- package/dist/agents/claude-code/types.d.ts +180 -0
- package/dist/agents/claude-code/types.js +112 -0
- package/dist/agents/codex/adapter.d.ts +6 -0
- package/dist/agents/codex/adapter.js +62 -0
- package/dist/agents/codex/item-parsers.d.ts +17 -0
- package/dist/agents/codex/item-parsers.js +126 -0
- package/dist/agents/codex/parse-event.d.ts +38 -0
- package/dist/agents/codex/parse-event.js +141 -0
- package/dist/agents/codex/types.d.ts +407 -0
- package/dist/agents/codex/types.js +188 -0
- package/dist/agents/copilot/adapter.d.ts +11 -0
- package/dist/agents/copilot/adapter.js +81 -0
- package/dist/agents/copilot/parse-event.d.ts +20 -0
- package/dist/agents/copilot/parse-event.js +137 -0
- package/dist/agents/copilot/stream-session.d.ts +10 -0
- package/dist/agents/copilot/stream-session.js +120 -0
- package/dist/agents/copilot/tail-file.d.ts +11 -0
- package/dist/agents/copilot/tail-file.js +52 -0
- package/dist/agents/copilot/transform-event.d.ts +19 -0
- package/dist/agents/copilot/transform-event.js +100 -0
- package/dist/agents/copilot/types.d.ts +320 -0
- package/dist/agents/copilot/types.js +220 -0
- package/dist/agents/copilot/watch-session.d.ts +26 -0
- package/dist/agents/copilot/watch-session.js +186 -0
- package/dist/agents/gemini/adapter.d.ts +6 -0
- package/dist/agents/gemini/adapter.js +78 -0
- package/dist/agents/gemini/parse-event.d.ts +18 -0
- package/dist/agents/gemini/parse-event.js +144 -0
- package/dist/agents/gemini/types.d.ts +162 -0
- package/dist/agents/gemini/types.js +103 -0
- package/dist/agents/opencode/adapter.d.ts +16 -0
- package/dist/agents/opencode/adapter.js +142 -0
- package/dist/agents/opencode/cleanup-session.d.ts +17 -0
- package/dist/agents/opencode/cleanup-session.js +41 -0
- package/dist/agents/opencode/create-session-start-event.d.ts +18 -0
- package/dist/agents/opencode/create-session-start-event.js +24 -0
- package/dist/agents/opencode/detect-empty-session.d.ts +25 -0
- package/dist/agents/opencode/detect-empty-session.js +42 -0
- package/dist/agents/opencode/map-error-to-event.d.ts +10 -0
- package/dist/agents/opencode/map-error-to-event.js +55 -0
- package/dist/agents/opencode/parse-message-part.d.ts +24 -0
- package/dist/agents/opencode/parse-message-part.js +112 -0
- package/dist/agents/opencode/parse-sse-event.d.ts +23 -0
- package/dist/agents/opencode/parse-sse-event.js +125 -0
- package/dist/agents/opencode/process-sse-events.d.ts +24 -0
- package/dist/agents/opencode/process-sse-events.js +66 -0
- package/dist/agents/opencode/server-types.d.ts +509 -0
- package/dist/agents/opencode/server-types.js +293 -0
- package/dist/agents/opencode/session-api.d.ts +56 -0
- package/dist/agents/opencode/session-api.js +151 -0
- package/dist/agents/opencode/spawn-server.d.ts +29 -0
- package/dist/agents/opencode/spawn-server.js +95 -0
- package/dist/agents/opencode/sse-client.d.ts +33 -0
- package/dist/agents/opencode/sse-client.js +145 -0
- package/dist/agents/registry.d.ts +15 -0
- package/dist/agents/registry.js +24 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +119 -0
- package/dist/credentials/get-credential-environment.d.ts +13 -0
- package/dist/credentials/get-credential-environment.js +46 -0
- package/dist/credentials/install-credentials.d.ts +27 -0
- package/dist/credentials/install-credentials.js +102 -0
- package/dist/credentials/save-json-file.d.ts +11 -0
- package/dist/credentials/save-json-file.js +21 -0
- package/dist/credentials/types.d.ts +24 -0
- package/dist/credentials/types.js +4 -0
- package/dist/credentials/write-agent-credentials.d.ts +36 -0
- package/dist/credentials/write-agent-credentials.js +91 -0
- package/dist/determine-session-success.d.ts +15 -0
- package/dist/determine-session-success.js +25 -0
- package/dist/format-event-tsv.d.ts +23 -0
- package/dist/format-event-tsv.js +136 -0
- package/dist/parse-credentials.d.ts +13 -0
- package/dist/parse-credentials.js +63 -0
- package/dist/parse-iso-timestamp.d.ts +21 -0
- package/dist/parse-iso-timestamp.js +37 -0
- package/dist/resolve-binary.d.ts +29 -0
- package/dist/resolve-binary.js +46 -0
- package/dist/resolve-output-mode.d.ts +39 -0
- package/dist/resolve-output-mode.js +39 -0
- package/dist/run-agent.d.ts +32 -0
- package/dist/run-agent.js +146 -0
- package/dist/stream-agent.d.ts +20 -0
- package/dist/stream-agent.js +207 -0
- package/dist/types/adapter.d.ts +101 -0
- package/dist/types/adapter.js +14 -0
- package/dist/types/events.d.ts +82 -0
- package/dist/types/events.js +13 -0
- package/dist/types/options.d.ts +41 -0
- package/dist/types/options.js +4 -0
- package/dist/validate-agent.d.ts +20 -0
- package/dist/validate-agent.js +50 -0
- package/dist/write-event.d.ts +23 -0
- package/dist/write-event.js +43 -0
- package/package.json +79 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE client for OpenCode server mode.
|
|
3
|
+
*
|
|
4
|
+
* Connects to the OpenCode server's /event endpoint and streams
|
|
5
|
+
* parsed SSE events.
|
|
6
|
+
*/
|
|
7
|
+
import { OpenCodeSSEEvent as OpenCodeSSEEventSchema } from "./server-types.js";
|
|
8
|
+
/** Error when SSE connection fails */
|
|
9
|
+
class SSEConnectionError extends Error {
|
|
10
|
+
statusCode;
|
|
11
|
+
constructor(message, statusCode) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "SSEConnectionError";
|
|
14
|
+
this.statusCode = statusCode;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Connects to OpenCode server's SSE endpoint and yields events.
|
|
19
|
+
*
|
|
20
|
+
* The generator will yield events until:
|
|
21
|
+
* - The connection is closed by the server
|
|
22
|
+
* - The abort signal is triggered
|
|
23
|
+
* - An error occurs
|
|
24
|
+
*/
|
|
25
|
+
async function* streamSSE(options) {
|
|
26
|
+
const url = `${options.baseUrl}/event`;
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
headers: {
|
|
29
|
+
Accept: "text/event-stream",
|
|
30
|
+
"Cache-Control": "no-cache",
|
|
31
|
+
"x-opencode-directory": options.directory,
|
|
32
|
+
},
|
|
33
|
+
signal: options.signal,
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new SSEConnectionError(`SSE connection failed: ${response.status} ${response.statusText}`, response.status);
|
|
37
|
+
}
|
|
38
|
+
if (!response.body) {
|
|
39
|
+
throw new SSEConnectionError("SSE response has no body");
|
|
40
|
+
}
|
|
41
|
+
const reader = response.body.getReader();
|
|
42
|
+
const decoder = new TextDecoder();
|
|
43
|
+
let buffer = "";
|
|
44
|
+
// Expose reader for manual cancellation
|
|
45
|
+
options.onReader?.(reader);
|
|
46
|
+
// Create a single abort promise that rejects when abort signal fires
|
|
47
|
+
// This is more efficient than creating a new promise on each iteration
|
|
48
|
+
const abortPromise = new Promise((_, reject) => {
|
|
49
|
+
if (options.signal?.aborted) {
|
|
50
|
+
reject(new Error("Aborted"));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
options.signal?.addEventListener("abort", () => {
|
|
54
|
+
reject(new Error("Aborted"));
|
|
55
|
+
}, { once: true });
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
for (;;) {
|
|
59
|
+
// Check abort signal before reading
|
|
60
|
+
if (options.signal?.aborted) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
// Race the read against the abort signal
|
|
64
|
+
let result;
|
|
65
|
+
try {
|
|
66
|
+
result = await Promise.race([reader.read(), abortPromise]);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Aborted
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (result.done) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
76
|
+
// Normalize CRLF to LF (SSE spec allows both line ending styles)
|
|
77
|
+
buffer = buffer.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
78
|
+
// Process complete SSE messages (separated by double newline)
|
|
79
|
+
const messages = buffer.split("\n\n");
|
|
80
|
+
buffer = messages.pop() ?? ""; // Keep incomplete message in buffer
|
|
81
|
+
for (const message of messages) {
|
|
82
|
+
// Check abort signal before yielding
|
|
83
|
+
if (options.signal?.aborted) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const event = parseSSEMessage(message);
|
|
87
|
+
if (event) {
|
|
88
|
+
yield event;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Process any remaining buffer (only if not aborted)
|
|
93
|
+
if (!options.signal?.aborted && buffer.trim()) {
|
|
94
|
+
const event = parseSSEMessage(buffer);
|
|
95
|
+
if (event) {
|
|
96
|
+
yield event;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
// Cancel any pending read
|
|
102
|
+
reader.cancel().catch(() => { });
|
|
103
|
+
reader.releaseLock();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Parses a single SSE message into an OpenCode event.
|
|
108
|
+
*
|
|
109
|
+
* SSE format:
|
|
110
|
+
* ```
|
|
111
|
+
* data: {"type": "...", "properties": {...}}
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
function parseSSEMessage(message) {
|
|
115
|
+
const lines = message.split("\n");
|
|
116
|
+
const dataLines = [];
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
if (line.startsWith("data: ")) {
|
|
119
|
+
dataLines.push(line.slice(6));
|
|
120
|
+
}
|
|
121
|
+
else if (line.startsWith("data:")) {
|
|
122
|
+
dataLines.push(line.slice(5));
|
|
123
|
+
}
|
|
124
|
+
// Ignore other SSE fields (event:, id:, retry:)
|
|
125
|
+
}
|
|
126
|
+
// Per SSE spec, multiple data lines are joined with newlines
|
|
127
|
+
const data = dataLines.join("\n");
|
|
128
|
+
if (!data) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const json = JSON.parse(data);
|
|
133
|
+
const result = OpenCodeSSEEventSchema.safeParse(json);
|
|
134
|
+
if (result.success) {
|
|
135
|
+
return result.data;
|
|
136
|
+
}
|
|
137
|
+
// Unknown event type - log for debugging but don't fail
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Invalid JSON - skip
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export { SSEConnectionError, streamSSE };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter registry for agent lookup and registration.
|
|
3
|
+
*
|
|
4
|
+
* Adapters self-register on import, allowing new agents to be added
|
|
5
|
+
* without modifying core code.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentAdapter, AgentInfo } from "../types/adapter.js";
|
|
8
|
+
import type { AgentCli } from "../types/events.js";
|
|
9
|
+
/** Registers an adapter in the registry */
|
|
10
|
+
declare function registerAdapter(adapter: AgentAdapter): void;
|
|
11
|
+
/** Retrieves an adapter by ID, or undefined if not found */
|
|
12
|
+
declare function getAdapter(id: AgentCli): AgentAdapter | undefined;
|
|
13
|
+
/** Returns metadata for all registered adapters */
|
|
14
|
+
declare function listAdapters(): AgentInfo[];
|
|
15
|
+
export { getAdapter, listAdapters, registerAdapter };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter registry for agent lookup and registration.
|
|
3
|
+
*
|
|
4
|
+
* Adapters self-register on import, allowing new agents to be added
|
|
5
|
+
* without modifying core code.
|
|
6
|
+
*/
|
|
7
|
+
const adapters = new Map();
|
|
8
|
+
/** Registers an adapter in the registry */
|
|
9
|
+
function registerAdapter(adapter) {
|
|
10
|
+
const { id } = adapter.info();
|
|
11
|
+
if (adapters.has(id)) {
|
|
12
|
+
throw new Error(`Adapter already registered: ${id}`);
|
|
13
|
+
}
|
|
14
|
+
adapters.set(id, adapter);
|
|
15
|
+
}
|
|
16
|
+
/** Retrieves an adapter by ID, or undefined if not found */
|
|
17
|
+
function getAdapter(id) {
|
|
18
|
+
return adapters.get(id);
|
|
19
|
+
}
|
|
20
|
+
/** Returns metadata for all registered adapters */
|
|
21
|
+
function listAdapters() {
|
|
22
|
+
return [...adapters.values()].map((adapter) => adapter.info());
|
|
23
|
+
}
|
|
24
|
+
export { getAdapter, listAdapters, registerAdapter };
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* axexec CLI - Unified agent execution with isolation.
|
|
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
|
|
10
|
+
*/
|
|
11
|
+
import "./agents/claude-code/adapter.js";
|
|
12
|
+
import "./agents/codex/adapter.js";
|
|
13
|
+
import "./agents/gemini/adapter.js";
|
|
14
|
+
import "./agents/opencode/adapter.js";
|
|
15
|
+
import "./agents/copilot/adapter.js";
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* axexec CLI - Unified agent execution with isolation.
|
|
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
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from "@commander-js/extra-typings";
|
|
12
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
13
|
+
// Import all adapters to trigger self-registration
|
|
14
|
+
import "./agents/claude-code/adapter.js";
|
|
15
|
+
import "./agents/codex/adapter.js";
|
|
16
|
+
import "./agents/gemini/adapter.js";
|
|
17
|
+
import "./agents/opencode/adapter.js";
|
|
18
|
+
import "./agents/copilot/adapter.js";
|
|
19
|
+
import { listAdapters } from "./agents/registry.js";
|
|
20
|
+
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
|
+
}
|
|
28
|
+
const program = new Command()
|
|
29
|
+
.name(packageJson.name)
|
|
30
|
+
.description(packageJson.description)
|
|
31
|
+
.version(packageJson.version, "-V, --version")
|
|
32
|
+
.showHelpAfterError("(add --help for additional information)")
|
|
33
|
+
.showSuggestionAfterError()
|
|
34
|
+
.option("--list-agents", "List available agents")
|
|
35
|
+
.option("-a, --agent <id>", "Agent to use (required)")
|
|
36
|
+
.option("-p, --prompt <text>", "Prompt text")
|
|
37
|
+
.option("-m, --model <model>", "Model to use")
|
|
38
|
+
.option("--allow <perms>", "Permission rules to allow (comma-separated)")
|
|
39
|
+
.option("--deny <perms>", "Permission rules to deny (comma-separated)")
|
|
40
|
+
.option("-f, --format <fmt>", "Output format: jsonl, tsv (default: tsv, truncated on TTY)")
|
|
41
|
+
.option("--raw-log <file>", "Write raw agent output to file")
|
|
42
|
+
.option("--debug", "Enable debug mode")
|
|
43
|
+
.option("--verbose", "Show agent stderr output")
|
|
44
|
+
.option("--preserve-github-sha", "Preserve GITHUB_SHA env var (Gemini)")
|
|
45
|
+
.argument("[prompt]", "Prompt text (alternative to -p)")
|
|
46
|
+
.addHelpText("after", `
|
|
47
|
+
Requirements:
|
|
48
|
+
Install the agent CLIs you plan to use: claude, codex, gemini, opencode, copilot.
|
|
49
|
+
Override paths: AXEXEC_CLAUDE_PATH, AXEXEC_CODEX_PATH, AXEXEC_GEMINI_PATH, AXEXEC_OPENCODE_PATH, AXEXEC_COPILOT_PATH
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
axexec -a claude "Refactor auth flow"
|
|
53
|
+
axexec -a claude -f jsonl "Audit deps" | jq 'select(.type=="tool.call")'
|
|
54
|
+
axexec --list-agents | tail -n +2 | cut -f1
|
|
55
|
+
`)
|
|
56
|
+
.action(async (positionalPrompt, options) => {
|
|
57
|
+
// Handle --list-agents
|
|
58
|
+
if (options.listAgents) {
|
|
59
|
+
const agents = listAdapters();
|
|
60
|
+
process.stdout.write("ID\tNAME\tPACKAGE\n");
|
|
61
|
+
for (const agent of agents) {
|
|
62
|
+
process.stdout.write(`${agent.id}\t${agent.name}\t${agent.package}\n`);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Get prompt from flag, positional argument, or stdin (in priority order)
|
|
67
|
+
// Only read stdin if no prompt was provided via CLI arguments to avoid
|
|
68
|
+
// blocking in CI environments with open but unused stdin.
|
|
69
|
+
const needsStdinPrompt = !options.prompt && !positionalPrompt;
|
|
70
|
+
const stdinPrompt = needsStdinPrompt && !process.stdin.isTTY ? await readStdin() : undefined;
|
|
71
|
+
const prompt = (options.prompt ??
|
|
72
|
+
positionalPrompt ??
|
|
73
|
+
stdinPrompt)?.trimEnd();
|
|
74
|
+
if (!prompt) {
|
|
75
|
+
process.stderr.write("Error: prompt is required\n");
|
|
76
|
+
process.stderr.write("Usage: axexec --agent <id> <prompt>\n");
|
|
77
|
+
process.stderr.write("Try 'axexec --help' for more information.\n");
|
|
78
|
+
process.exitCode = 2;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!options.agent) {
|
|
82
|
+
process.stderr.write("Error: --agent is required\n");
|
|
83
|
+
process.stderr.write("Usage: axexec --agent <id> <prompt>\n");
|
|
84
|
+
process.stderr.write("Try 'axexec --help' for more information.\n");
|
|
85
|
+
process.exitCode = 2;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Validate format option
|
|
89
|
+
let format;
|
|
90
|
+
if (options.format) {
|
|
91
|
+
if (options.format === "jsonl" || options.format === "tsv") {
|
|
92
|
+
format = options.format;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
process.stderr.write(`Error: Invalid format '${options.format}'\n`);
|
|
96
|
+
process.stderr.write("Valid formats: jsonl, tsv\n");
|
|
97
|
+
process.exitCode = 2;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Run agent
|
|
102
|
+
const result = await runAgent(options.agent, {
|
|
103
|
+
prompt,
|
|
104
|
+
model: options.model,
|
|
105
|
+
allow: options.allow,
|
|
106
|
+
deny: options.deny,
|
|
107
|
+
format,
|
|
108
|
+
rawLog: options.rawLog,
|
|
109
|
+
debug: options.debug,
|
|
110
|
+
verbose: options.verbose,
|
|
111
|
+
preserveGithubSha: options.preserveGithubSha,
|
|
112
|
+
});
|
|
113
|
+
// Set exit code based on success
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// Parse CLI arguments
|
|
119
|
+
await program.parseAsync();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable helpers for credential-based auth.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentCli } from "axshared";
|
|
5
|
+
import type { RawCredentials } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Gets additional environment variables for credential-based auth.
|
|
8
|
+
*
|
|
9
|
+
* Some agents use environment variables for API keys in addition to
|
|
10
|
+
* or instead of file-based credentials.
|
|
11
|
+
*/
|
|
12
|
+
declare function getCredentialEnvironment(agentId: AgentCli, credentials: RawCredentials): Record<string, string>;
|
|
13
|
+
export { getCredentialEnvironment };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable helpers for credential-based auth.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Gets additional environment variables for credential-based auth.
|
|
6
|
+
*
|
|
7
|
+
* Some agents use environment variables for API keys in addition to
|
|
8
|
+
* or instead of file-based credentials.
|
|
9
|
+
*/
|
|
10
|
+
function getCredentialEnvironment(agentId, credentials) {
|
|
11
|
+
const environment = {};
|
|
12
|
+
switch (agentId) {
|
|
13
|
+
case "claude": {
|
|
14
|
+
if (credentials.apiKey) {
|
|
15
|
+
environment["ANTHROPIC_API_KEY"] = credentials.apiKey;
|
|
16
|
+
}
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case "codex": {
|
|
20
|
+
if (credentials.apiKey) {
|
|
21
|
+
environment["OPENAI_API_KEY"] = credentials.apiKey;
|
|
22
|
+
}
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case "gemini": {
|
|
26
|
+
if (credentials.apiKey) {
|
|
27
|
+
environment["GEMINI_API_KEY"] = credentials.apiKey;
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case "opencode": {
|
|
32
|
+
if (credentials.apiKey) {
|
|
33
|
+
environment["ANTHROPIC_API_KEY"] = credentials.apiKey;
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case "copilot": {
|
|
38
|
+
if (credentials.githubToken) {
|
|
39
|
+
environment["GITHUB_TOKEN"] = credentials.githubToken;
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return environment;
|
|
45
|
+
}
|
|
46
|
+
export { getCredentialEnvironment };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential installation for isolated agent execution.
|
|
3
|
+
*
|
|
4
|
+
* Writes raw credentials to a temporary directory in agent-specific formats,
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
import { type AgentCli } from "axshared";
|
|
10
|
+
import type { InstallResult, RawCredentials } from "./types.js";
|
|
11
|
+
export type { RawCredentials } from "./types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Installs credentials to a temporary directory and returns isolation env vars.
|
|
14
|
+
*
|
|
15
|
+
* This function:
|
|
16
|
+
* 1. Creates a temp directory structure appropriate for the agent
|
|
17
|
+
* 2. Writes credential files in the agent-specific format
|
|
18
|
+
* 3. Returns environment variables that point the agent at the temp directory
|
|
19
|
+
*
|
|
20
|
+
* The caller is responsible for cleaning up the temp directory after use.
|
|
21
|
+
*/
|
|
22
|
+
declare function installCredentials(agentId: AgentCli, credentials: RawCredentials): Promise<InstallResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Cleanup for use in finally blocks.
|
|
25
|
+
*/
|
|
26
|
+
declare function cleanupCredentials(baseDirectory: string): Promise<void>;
|
|
27
|
+
export { cleanupCredentials, installCredentials };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential installation for isolated agent execution.
|
|
3
|
+
*
|
|
4
|
+
* Writes raw credentials to a temporary directory in agent-specific formats,
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdirSync } from "node:fs";
|
|
10
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { buildAgentRuntimeEnvironment, getAgentConfigSubdirectory, } from "axshared";
|
|
14
|
+
import { getCredentialEnvironment } from "./get-credential-environment.js";
|
|
15
|
+
import { installClaudeCredentials, installCodexCredentials, installGeminiCredentials, installOpenCodeCredentials, } from "./write-agent-credentials.js";
|
|
16
|
+
/**
|
|
17
|
+
* Creates a temporary directory structure for an agent.
|
|
18
|
+
*/
|
|
19
|
+
async function createTemporaryDirectories(agentId) {
|
|
20
|
+
const baseDirectory = await mkdtemp(path.join(tmpdir(), `axexec-${agentId}-`));
|
|
21
|
+
const subdirectory = getAgentConfigSubdirectory(agentId);
|
|
22
|
+
let configDirectory;
|
|
23
|
+
let dataDirectory;
|
|
24
|
+
if (subdirectory) {
|
|
25
|
+
configDirectory = path.join(baseDirectory, subdirectory);
|
|
26
|
+
dataDirectory = configDirectory;
|
|
27
|
+
mkdirSync(configDirectory, { recursive: true, mode: 0o700 });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
configDirectory = baseDirectory;
|
|
31
|
+
dataDirectory = baseDirectory;
|
|
32
|
+
}
|
|
33
|
+
// OpenCode needs separate data directory
|
|
34
|
+
if (agentId === "opencode") {
|
|
35
|
+
dataDirectory = path.join(baseDirectory, "opencode");
|
|
36
|
+
if (dataDirectory !== configDirectory) {
|
|
37
|
+
mkdirSync(dataDirectory, { recursive: true, mode: 0o700 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { baseDirectory, configDirectory, dataDirectory };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Installs agent-specific credentials to the appropriate directory.
|
|
44
|
+
*/
|
|
45
|
+
function installAgentCredentials(agentId, configDirectory, dataDirectory, credentials) {
|
|
46
|
+
switch (agentId) {
|
|
47
|
+
case "claude": {
|
|
48
|
+
installClaudeCredentials(configDirectory, credentials);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case "codex": {
|
|
52
|
+
installCodexCredentials(configDirectory, credentials);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case "gemini": {
|
|
56
|
+
installGeminiCredentials(configDirectory, credentials);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case "opencode": {
|
|
60
|
+
installOpenCodeCredentials(dataDirectory, credentials);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "copilot": {
|
|
64
|
+
// Copilot uses GITHUB_TOKEN env var, not file-based credentials
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Installs credentials to a temporary directory and returns isolation env vars.
|
|
71
|
+
*
|
|
72
|
+
* This function:
|
|
73
|
+
* 1. Creates a temp directory structure appropriate for the agent
|
|
74
|
+
* 2. Writes credential files in the agent-specific format
|
|
75
|
+
* 3. Returns environment variables that point the agent at the temp directory
|
|
76
|
+
*
|
|
77
|
+
* The caller is responsible for cleaning up the temp directory after use.
|
|
78
|
+
*/
|
|
79
|
+
async function installCredentials(agentId, credentials) {
|
|
80
|
+
const { baseDirectory, configDirectory, dataDirectory } = await createTemporaryDirectories(agentId);
|
|
81
|
+
installAgentCredentials(agentId, configDirectory, dataDirectory, credentials);
|
|
82
|
+
const runtimeEnvironment = buildAgentRuntimeEnvironment(agentId, configDirectory, dataDirectory);
|
|
83
|
+
const credentialEnvironment = getCredentialEnvironment(agentId, credentials);
|
|
84
|
+
return {
|
|
85
|
+
env: { ...runtimeEnvironment, ...credentialEnvironment },
|
|
86
|
+
configDirectory,
|
|
87
|
+
dataDirectory,
|
|
88
|
+
baseDirectory,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Cleanup for use in finally blocks.
|
|
93
|
+
*/
|
|
94
|
+
async function cleanupCredentials(baseDirectory) {
|
|
95
|
+
try {
|
|
96
|
+
await rm(baseDirectory, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Ignore cleanup errors - best effort
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export { cleanupCredentials, installCredentials };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic JSON file writing utility.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Saves JSON data to a file atomically.
|
|
6
|
+
*
|
|
7
|
+
* Creates parent directories if needed, writes to a temp file first,
|
|
8
|
+
* then atomically renames to the target path.
|
|
9
|
+
*/
|
|
10
|
+
declare function saveJsonFile(filePath: string, data: unknown, mode?: number): void;
|
|
11
|
+
export { saveJsonFile };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic JSON file writing utility.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
/**
|
|
7
|
+
* Saves JSON data to a file atomically.
|
|
8
|
+
*
|
|
9
|
+
* Creates parent directories if needed, writes to a temp file first,
|
|
10
|
+
* then atomically renames to the target path.
|
|
11
|
+
*/
|
|
12
|
+
function saveJsonFile(filePath, data, mode = 0o600) {
|
|
13
|
+
const directory = path.dirname(filePath);
|
|
14
|
+
if (!existsSync(directory)) {
|
|
15
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
16
|
+
}
|
|
17
|
+
const temporaryFile = `${filePath}.tmp.${Date.now()}`;
|
|
18
|
+
writeFileSync(temporaryFile, JSON.stringify(data, undefined, 2), { mode });
|
|
19
|
+
renameSync(temporaryFile, filePath);
|
|
20
|
+
}
|
|
21
|
+
export { saveJsonFile };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential types for axexec.
|
|
3
|
+
*/
|
|
4
|
+
/** Raw credentials that can be installed for any agent */
|
|
5
|
+
interface RawCredentials {
|
|
6
|
+
/** OAuth credentials (access token, refresh token, etc.) */
|
|
7
|
+
oauth?: Record<string, unknown>;
|
|
8
|
+
/** API key for direct API access */
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
/** GitHub token for Copilot */
|
|
11
|
+
githubToken?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Result of credential installation */
|
|
14
|
+
interface InstallResult {
|
|
15
|
+
/** Environment variables to pass to the agent subprocess */
|
|
16
|
+
env: Record<string, string>;
|
|
17
|
+
/** Configuration directory path (for cleanup) */
|
|
18
|
+
configDirectory: string;
|
|
19
|
+
/** Data directory path (for cleanup, may equal configDirectory) */
|
|
20
|
+
dataDirectory: string;
|
|
21
|
+
/** Base temporary directory (for cleanup) */
|
|
22
|
+
baseDirectory: string;
|
|
23
|
+
}
|
|
24
|
+
export type { InstallResult, RawCredentials };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent credential file writers.
|
|
3
|
+
*
|
|
4
|
+
* Each agent has its own credential file format. These functions
|
|
5
|
+
* write credentials in the agent-specific format.
|
|
6
|
+
*/
|
|
7
|
+
import type { RawCredentials } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Installs Claude credentials to a config directory.
|
|
10
|
+
*
|
|
11
|
+
* File: .credentials.json
|
|
12
|
+
* Format: { claudeAiOauth: { ... } }
|
|
13
|
+
*/
|
|
14
|
+
declare function installClaudeCredentials(configDirectory: string, credentials: RawCredentials): void;
|
|
15
|
+
/**
|
|
16
|
+
* Installs Codex credentials to a config directory.
|
|
17
|
+
*
|
|
18
|
+
* File: auth.json
|
|
19
|
+
* Format: { OPENAI_API_KEY: "..." } or { tokens: { ... }, last_refresh: "..." }
|
|
20
|
+
*/
|
|
21
|
+
declare function installCodexCredentials(configDirectory: string, credentials: RawCredentials): void;
|
|
22
|
+
/**
|
|
23
|
+
* Installs Gemini credentials to a config directory.
|
|
24
|
+
*
|
|
25
|
+
* File: oauth_creds.json
|
|
26
|
+
* Format: { access_token: "...", refresh_token: "...", ... }
|
|
27
|
+
*/
|
|
28
|
+
declare function installGeminiCredentials(configDirectory: string, credentials: RawCredentials): void;
|
|
29
|
+
/**
|
|
30
|
+
* Installs OpenCode credentials to a data directory.
|
|
31
|
+
*
|
|
32
|
+
* File: auth.json
|
|
33
|
+
* Format: { anthropic: { type: "oauth", ... }, openai: { type: "api", ... } }
|
|
34
|
+
*/
|
|
35
|
+
declare function installOpenCodeCredentials(dataDirectory: string, credentials: RawCredentials): void;
|
|
36
|
+
export { installClaudeCredentials, installCodexCredentials, installGeminiCredentials, installOpenCodeCredentials, };
|