@agentstep/agent-sdk 0.1.0 → 0.2.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/package.json +1 -1
- package/src/db/agents.ts +2 -2
- package/src/handlers/agents.ts +9 -3
- package/src/handlers/environments.ts +8 -2
- package/src/handlers/ui/app.js +44 -27
- package/src/handlers/ui.ts +45 -28
- package/src/openapi/schemas.ts +6 -6
- package/src/providers/{apple.ts → apple-container.ts} +1 -1
- package/src/providers/apple-firecracker.ts +212 -0
- package/src/providers/registry.ts +2 -1
- package/src/providers/types.ts +1 -1
- package/src/sessions/driver.ts +2 -2
- package/src/sprite/lifecycle.ts +2 -2
- package/src/types.ts +4 -2
package/package.json
CHANGED
package/src/db/agents.ts
CHANGED
|
@@ -19,7 +19,7 @@ function hydrate(row: AgentRow, ver: AgentVersionRow): Agent {
|
|
|
19
19
|
system: ver.system,
|
|
20
20
|
tools: JSON.parse(ver.tools_json) as ToolConfig[],
|
|
21
21
|
mcp_servers: JSON.parse(ver.mcp_servers_json) as Record<string, McpServerConfig>,
|
|
22
|
-
|
|
22
|
+
engine: (ver.backend ?? "claude") as BackendName,
|
|
23
23
|
webhook_url: ver.webhook_url ?? null,
|
|
24
24
|
webhook_events: ver.webhook_events_json ? (JSON.parse(ver.webhook_events_json) as string[]) : ["session.status_idle", "session.status_running", "session.error"],
|
|
25
25
|
threads_enabled: Boolean(ver.threads_enabled),
|
|
@@ -129,7 +129,7 @@ export function updateAgent(
|
|
|
129
129
|
input.system ?? existing.system,
|
|
130
130
|
JSON.stringify(input.tools ?? existing.tools),
|
|
131
131
|
JSON.stringify(input.mcp_servers ?? existing.mcp_servers),
|
|
132
|
-
existing.
|
|
132
|
+
existing.engine,
|
|
133
133
|
input.webhook_url !== undefined ? input.webhook_url : existing.webhook_url,
|
|
134
134
|
JSON.stringify(input.webhook_events ?? existing.webhook_events),
|
|
135
135
|
input.threads_enabled !== undefined ? (input.threads_enabled ? 1 : 0) : (existing.threads_enabled ? 1 : 0),
|
package/src/handlers/agents.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { createAgent, getAgent, updateAgent, archiveAgent, listAgents } from "..
|
|
|
4
4
|
import { resolveBackend } from "../backends/registry";
|
|
5
5
|
import { isProxied, markProxied, unmarkProxied } from "../db/proxy";
|
|
6
6
|
import { forwardToAnthropic, validateAnthropicProxy } from "../proxy/forward";
|
|
7
|
-
import { badRequest, notFound } from "../errors";
|
|
7
|
+
import { badRequest, notFound, conflict } from "../errors";
|
|
8
8
|
|
|
9
9
|
const ToolSchema = z.union([
|
|
10
10
|
z.object({
|
|
@@ -39,7 +39,7 @@ const CreateSchema = z.object({
|
|
|
39
39
|
system: z.string().nullish(),
|
|
40
40
|
tools: z.array(ToolSchema).optional(),
|
|
41
41
|
mcp_servers: McpServerSchema.optional(),
|
|
42
|
-
|
|
42
|
+
engine: z.enum(["claude", "opencode", "codex", "anthropic", "gemini", "factory"]).optional(),
|
|
43
43
|
webhook_url: z.string().url().optional(),
|
|
44
44
|
webhook_events: z.array(z.string()).optional(),
|
|
45
45
|
threads_enabled: z.boolean().optional(),
|
|
@@ -76,7 +76,13 @@ export function handleCreateAgent(request: Request): Promise<Response> {
|
|
|
76
76
|
throw badRequest(`invalid body: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
// Check for duplicate name
|
|
80
|
+
const existing = listAgents({ limit: 1000 });
|
|
81
|
+
if (existing.some(a => a.name === parsed.data.name)) {
|
|
82
|
+
throw conflict(`Agent with name "${parsed.data.name}" already exists`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const backendName = parsed.data.engine ?? "claude";
|
|
80
86
|
|
|
81
87
|
if (backendName === "anthropic") {
|
|
82
88
|
const proxyErr = validateAnthropicProxy();
|
|
@@ -37,7 +37,7 @@ const NetworkingSchema = z.union([
|
|
|
37
37
|
|
|
38
38
|
const ConfigSchema = z.object({
|
|
39
39
|
type: z.literal("cloud"),
|
|
40
|
-
provider: z.enum(["sprites", "docker", "apple", "podman", "e2b", "vercel", "daytona", "fly", "modal"]).optional(),
|
|
40
|
+
provider: z.enum(["sprites", "docker", "apple-container", "apple-firecracker", "podman", "e2b", "vercel", "daytona", "fly", "modal"]).optional(),
|
|
41
41
|
packages: PackagesSchema,
|
|
42
42
|
networking: NetworkingSchema.optional(),
|
|
43
43
|
});
|
|
@@ -68,6 +68,12 @@ export function handleCreateEnvironment(request: Request): Promise<Response> {
|
|
|
68
68
|
return proxyRes;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Check for duplicate name
|
|
72
|
+
const existingEnvs = listEnvironments({ limit: 1000 });
|
|
73
|
+
if (existingEnvs.some(e => e.name === parsed.data.name)) {
|
|
74
|
+
throw conflict(`Environment with name "${parsed.data.name}" already exists`);
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
// Pre-flight: check provider is available before creating the environment
|
|
72
78
|
const providerName = parsed.data.config.provider ?? "sprites";
|
|
73
79
|
const provider = await resolveProvider(providerName);
|
|
@@ -128,7 +134,7 @@ export function handleDeleteEnvironment(request: Request, id: string): Promise<R
|
|
|
128
134
|
const env = getEnvironment(id);
|
|
129
135
|
if (!env) throw notFound(`environment ${id} not found`);
|
|
130
136
|
if (hasSessionsAttached(id)) {
|
|
131
|
-
throw conflict(`
|
|
137
|
+
throw conflict(`Cannot delete: environment has active sessions. Archive or delete sessions first.`);
|
|
132
138
|
}
|
|
133
139
|
deleteEnvironment(id);
|
|
134
140
|
return jsonOk({ id, type: "environment_deleted" });
|
package/src/handlers/ui/app.js
CHANGED
|
@@ -32,7 +32,7 @@ let lastAppendedRole = "";
|
|
|
32
32
|
|
|
33
33
|
let onboardingStep = 0;
|
|
34
34
|
let onboardVaultId = null;
|
|
35
|
-
let
|
|
35
|
+
let onboardEngine = "claude";
|
|
36
36
|
|
|
37
37
|
// ── Init ──
|
|
38
38
|
document.getElementById("apiKeyInput").value = apiKey;
|
|
@@ -115,11 +115,11 @@ function renderOnboarding() {
|
|
|
115
115
|
if (onboardingStep === 0) {
|
|
116
116
|
content = `
|
|
117
117
|
<h2 style="font-size:18px;font-weight:600;color:var(--heading)">Create your first agent</h2>
|
|
118
|
-
<p style="font-size:13px;color:var(--muted);margin-top:4px">Pick
|
|
118
|
+
<p style="font-size:13px;color:var(--muted);margin-top:4px">Pick an engine and model to get started.</p>
|
|
119
119
|
<div style="display:flex;flex-direction:column;gap:12px;margin-top:16px;width:100%">
|
|
120
120
|
<div class="form-group"><label class="form-label">Name</label><input class="form-input" id="obAgentName" value="Coder" style="width:100%" /></div>
|
|
121
|
-
<div class="form-group"><label class="form-label">
|
|
122
|
-
<select class="form-select" id="
|
|
121
|
+
<div class="form-group"><label class="form-label">Engine</label>
|
|
122
|
+
<select class="form-select" id="obEngine" onchange="updateObModels()" style="width:100%">
|
|
123
123
|
<option value="claude">Claude — Max subscription or API key</option>
|
|
124
124
|
<option value="opencode">OpenCode — Multi-provider</option>
|
|
125
125
|
<option value="codex">Codex — GPT-5.4 models</option>
|
|
@@ -138,9 +138,20 @@ function renderOnboarding() {
|
|
|
138
138
|
<div class="form-group"><label class="form-label">Name</label><input class="form-input" id="obEnvName" value="dev" style="width:100%" /></div>
|
|
139
139
|
<div class="form-group"><label class="form-label">Provider</label>
|
|
140
140
|
<select class="form-select" id="obProvider" onchange="onboardProvider=this.value" style="width:100%">
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
<optgroup label="Local">
|
|
142
|
+
<option value="docker">Docker</option>
|
|
143
|
+
<option value="apple-container">Apple Container (macOS 26+)</option>
|
|
144
|
+
<option value="apple-firecracker">AgentStep Firecracker microVM (M3+)</option>
|
|
145
|
+
<option value="podman">Podman</option>
|
|
146
|
+
</optgroup>
|
|
147
|
+
<optgroup label="Cloud">
|
|
148
|
+
<option value="sprites">sprites.dev</option>
|
|
149
|
+
<option value="e2b">E2B</option>
|
|
150
|
+
<option value="fly">Fly.io</option>
|
|
151
|
+
<option value="vercel">Vercel</option>
|
|
152
|
+
<option value="daytona">Daytona</option>
|
|
153
|
+
<option value="modal">Modal</option>
|
|
154
|
+
</optgroup>
|
|
144
155
|
</select>
|
|
145
156
|
</div>
|
|
146
157
|
<button class="btn btn-primary" onclick="onboardCreateEnv()" style="width:100%">Create Environment</button>
|
|
@@ -150,7 +161,7 @@ function renderOnboarding() {
|
|
|
150
161
|
const fields = [];
|
|
151
162
|
const backendKeys = { claude: [{ key: "ANTHROPIC_API_KEY", label: "Anthropic API Key", placeholder: "sk-ant-...", alt: "or CLAUDE_CODE_OAUTH_TOKEN" }], opencode: [{ key: "OPENAI_API_KEY", label: "OpenAI API Key", placeholder: "sk-..." }], codex: [{ key: "OPENAI_API_KEY", label: "OpenAI API Key", placeholder: "sk-..." }], gemini: [{ key: "GEMINI_API_KEY", label: "Gemini API Key", placeholder: "AIza..." }], factory: [{ key: "FACTORY_API_KEY", label: "Factory API Key", placeholder: "fk-..." }] };
|
|
152
163
|
const providerKeys = { sprites: [{ key: "SPRITE_TOKEN", label: "Sprites.dev Token", placeholder: "user/org/.../token" }] };
|
|
153
|
-
(backendKeys[
|
|
164
|
+
(backendKeys[onboardEngine] || []).forEach(f => fields.push(f));
|
|
154
165
|
(providerKeys[onboardProvider] || []).forEach(f => fields.push(f));
|
|
155
166
|
|
|
156
167
|
const fieldHtml = fields.map((f, i) => `
|
|
@@ -173,7 +184,7 @@ function renderOnboarding() {
|
|
|
173
184
|
<p style="font-size:13px;color:var(--muted);margin-top:4px">Your agent and environment are set up.</p>
|
|
174
185
|
<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px;margin-top:16px;width:100%;font-size:13px;display:flex;flex-direction:column;gap:6px">
|
|
175
186
|
<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Agent</span><span style="color:var(--heading)">${esc(agents[0]?.name || "")}</span></div>
|
|
176
|
-
<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">
|
|
187
|
+
<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Engine</span><span style="font-family:var(--mono);font-size:11px">${agents[0]?.engine || ""}</span></div>
|
|
177
188
|
<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Model</span><span style="font-family:var(--mono);font-size:11px">${agents[0]?.model || ""}</span></div>
|
|
178
189
|
<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Environment</span><span style="color:var(--heading)">${esc(environments[0]?.name || "")}</span></div>
|
|
179
190
|
${onboardVaultId ? '<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Secrets</span><span style="color:var(--success)">✓ Vault configured</span></div>' : '<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Secrets</span><span style="color:var(--dim)">Using server .env</span></div>'}
|
|
@@ -191,7 +202,7 @@ function renderOnboarding() {
|
|
|
191
202
|
}
|
|
192
203
|
|
|
193
204
|
function updateObModels() {
|
|
194
|
-
const backend = document.getElementById("
|
|
205
|
+
const backend = document.getElementById("obEngine")?.value || "claude";
|
|
195
206
|
const models = MODELS[backend] || MODELS.claude;
|
|
196
207
|
const el = document.getElementById("obModel");
|
|
197
208
|
if (el) el.innerHTML = models.map(m => `<option value="${m}">${m}</option>`).join("");
|
|
@@ -199,16 +210,16 @@ function updateObModels() {
|
|
|
199
210
|
|
|
200
211
|
async function onboardCreateAgent() {
|
|
201
212
|
const name = document.getElementById("obAgentName")?.value?.trim();
|
|
202
|
-
const backend = document.getElementById("
|
|
213
|
+
const backend = document.getElementById("obEngine")?.value;
|
|
203
214
|
const model = document.getElementById("obModel")?.value;
|
|
204
215
|
if (!name) return;
|
|
205
216
|
try {
|
|
206
|
-
const body = { name, model, backend };
|
|
217
|
+
const body = { name, model, engine: backend };
|
|
207
218
|
if (backend === "claude") body.tools = [{ type: "agent_toolset_20260401" }];
|
|
208
219
|
await api("/v1/agents", { method: "POST", body: JSON.stringify(body) });
|
|
209
220
|
const a = await api("/v1/agents?limit=50");
|
|
210
221
|
agents = a.data || [];
|
|
211
|
-
|
|
222
|
+
onboardEngine = backend;
|
|
212
223
|
onboardingStep = 1; // → environment step
|
|
213
224
|
renderOnboarding();
|
|
214
225
|
showToast("Agent created");
|
|
@@ -538,7 +549,7 @@ function renderAgents() {
|
|
|
538
549
|
if (!agents.length) { el.innerHTML = '<p style="color:var(--dim);font-size:12px;padding:8px">No agents yet</p>'; return; }
|
|
539
550
|
el.innerHTML = agents.map((a) => `
|
|
540
551
|
<div class="card-item">
|
|
541
|
-
<div><div class="name">${esc(a.name)}</div><div class="detail">${esc(a.model)} / ${a.
|
|
552
|
+
<div><div class="name">${esc(a.name)}</div><div class="detail">${esc(a.model)} / ${a.engine}</div></div>
|
|
542
553
|
<div style="display:flex;gap:4px">
|
|
543
554
|
<button class="btn btn-sm btn-secondary" onclick="showAgentConfig('${esc(a.id)}')">Config</button>
|
|
544
555
|
<button class="btn btn-sm btn-danger" onclick="deleteAgent('${esc(a.id)}')">Delete</button>
|
|
@@ -614,8 +625,8 @@ function showCreateAgentModal() {
|
|
|
614
625
|
<div class="modal">
|
|
615
626
|
<h2>Create Agent</h2>
|
|
616
627
|
<div class="form-group"><label class="form-label">Name</label><input class="form-input" id="agentName" value="Coder" /></div>
|
|
617
|
-
<div class="form-group"><label class="form-label">
|
|
618
|
-
<select class="form-select" id="
|
|
628
|
+
<div class="form-group"><label class="form-label">Engine</label>
|
|
629
|
+
<select class="form-select" id="agentEngine" onchange="updateModelOptions()">
|
|
619
630
|
<option value="claude">Claude</option><option value="opencode">OpenCode</option><option value="codex">Codex</option><option value="gemini">Gemini</option><option value="factory">Factory</option>
|
|
620
631
|
</select>
|
|
621
632
|
</div>
|
|
@@ -630,18 +641,18 @@ function showCreateAgentModal() {
|
|
|
630
641
|
}
|
|
631
642
|
|
|
632
643
|
function updateModelOptions() {
|
|
633
|
-
const backend = document.getElementById("
|
|
644
|
+
const backend = document.getElementById("agentEngine").value;
|
|
634
645
|
const models = MODELS[backend] || MODELS.claude;
|
|
635
646
|
document.getElementById("agentModel").innerHTML = models.map((m) => `<option value="${m}">${m}</option>`).join("");
|
|
636
647
|
}
|
|
637
648
|
|
|
638
649
|
async function createAgent() {
|
|
639
650
|
const name = document.getElementById("agentName").value.trim();
|
|
640
|
-
const backend = document.getElementById("
|
|
651
|
+
const backend = document.getElementById("agentEngine").value;
|
|
641
652
|
const model = document.getElementById("agentModel").value;
|
|
642
653
|
if (!name) return;
|
|
643
654
|
try {
|
|
644
|
-
const body = { name, model, backend };
|
|
655
|
+
const body = { name, model, engine: backend };
|
|
645
656
|
if (backend === "claude") body.tools = [{ type: "agent_toolset_20260401" }];
|
|
646
657
|
await api("/v1/agents", { method: "POST", body: JSON.stringify(body) });
|
|
647
658
|
closeModal(); loadResources(); showToast("Agent created");
|
|
@@ -660,14 +671,20 @@ function showCreateEnvModal() {
|
|
|
660
671
|
<div class="form-group"><label class="form-label">Name</label><input class="form-input" id="envName" value="dev" style="width:100%" /></div>
|
|
661
672
|
<div class="form-group"><label class="form-label">Provider</label>
|
|
662
673
|
<select class="form-select" id="envProvider" onchange="toggleEnvToken()" style="width:100%">
|
|
663
|
-
<
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
<
|
|
670
|
-
|
|
674
|
+
<optgroup label="Local">
|
|
675
|
+
<option value="docker">Docker</option>
|
|
676
|
+
<option value="apple-container">Apple Container (macOS 26+)</option>
|
|
677
|
+
<option value="apple-firecracker">AgentStep Firecracker microVM (M3+)</option>
|
|
678
|
+
<option value="podman">Podman</option>
|
|
679
|
+
</optgroup>
|
|
680
|
+
<optgroup label="Cloud">
|
|
681
|
+
<option value="sprites">sprites.dev</option>
|
|
682
|
+
<option value="e2b">E2B</option>
|
|
683
|
+
<option value="fly">Fly.io</option>
|
|
684
|
+
<option value="vercel">Vercel</option>
|
|
685
|
+
<option value="daytona">Daytona</option>
|
|
686
|
+
<option value="modal">Modal</option>
|
|
687
|
+
</optgroup>
|
|
671
688
|
</select>
|
|
672
689
|
</div>
|
|
673
690
|
<div class="form-group" id="envTokenGroup" style="display:none">
|
package/src/handlers/ui.ts
CHANGED
|
@@ -308,7 +308,7 @@ let lastAppendedRole = "";
|
|
|
308
308
|
|
|
309
309
|
let onboardingStep = 0;
|
|
310
310
|
let onboardVaultId = null;
|
|
311
|
-
let
|
|
311
|
+
let onboardEngine = "claude";
|
|
312
312
|
|
|
313
313
|
// ── Init ──
|
|
314
314
|
document.getElementById("apiKeyInput").value = apiKey;
|
|
@@ -391,11 +391,11 @@ function renderOnboarding() {
|
|
|
391
391
|
if (onboardingStep === 0) {
|
|
392
392
|
content = \`
|
|
393
393
|
<h2 style="font-size:18px;font-weight:600;color:var(--heading)">Create your first agent</h2>
|
|
394
|
-
<p style="font-size:13px;color:var(--muted);margin-top:4px">Pick
|
|
394
|
+
<p style="font-size:13px;color:var(--muted);margin-top:4px">Pick an engine and model to get started.</p>
|
|
395
395
|
<div style="display:flex;flex-direction:column;gap:12px;margin-top:16px;width:100%">
|
|
396
396
|
<div class="form-group"><label class="form-label">Name</label><input class="form-input" id="obAgentName" value="Coder" style="width:100%" /></div>
|
|
397
|
-
<div class="form-group"><label class="form-label">
|
|
398
|
-
<select class="form-select" id="
|
|
397
|
+
<div class="form-group"><label class="form-label">Engine</label>
|
|
398
|
+
<select class="form-select" id="obEngine" onchange="updateObModels()" style="width:100%">
|
|
399
399
|
<option value="claude">Claude — Max subscription or API key</option>
|
|
400
400
|
<option value="opencode">OpenCode — Multi-provider</option>
|
|
401
401
|
<option value="codex">Codex — GPT-5.4 models</option>
|
|
@@ -414,9 +414,20 @@ function renderOnboarding() {
|
|
|
414
414
|
<div class="form-group"><label class="form-label">Name</label><input class="form-input" id="obEnvName" value="dev" style="width:100%" /></div>
|
|
415
415
|
<div class="form-group"><label class="form-label">Provider</label>
|
|
416
416
|
<select class="form-select" id="obProvider" onchange="onboardProvider=this.value" style="width:100%">
|
|
417
|
-
<
|
|
418
|
-
|
|
419
|
-
|
|
417
|
+
<optgroup label="Local">
|
|
418
|
+
<option value="docker">Docker</option>
|
|
419
|
+
<option value="apple-container">Apple Container (macOS 26+)</option>
|
|
420
|
+
<option value="apple-firecracker">AgentStep Firecracker microVM (M3+)</option>
|
|
421
|
+
<option value="podman">Podman</option>
|
|
422
|
+
</optgroup>
|
|
423
|
+
<optgroup label="Cloud">
|
|
424
|
+
<option value="sprites">sprites.dev</option>
|
|
425
|
+
<option value="e2b">E2B</option>
|
|
426
|
+
<option value="fly">Fly.io</option>
|
|
427
|
+
<option value="vercel">Vercel</option>
|
|
428
|
+
<option value="daytona">Daytona</option>
|
|
429
|
+
<option value="modal">Modal</option>
|
|
430
|
+
</optgroup>
|
|
420
431
|
</select>
|
|
421
432
|
</div>
|
|
422
433
|
<button class="btn btn-primary" onclick="onboardCreateEnv()" style="width:100%">Create Environment</button>
|
|
@@ -426,7 +437,7 @@ function renderOnboarding() {
|
|
|
426
437
|
const fields = [];
|
|
427
438
|
const backendKeys = { claude: [{ key: "ANTHROPIC_API_KEY", label: "Anthropic API Key", placeholder: "sk-ant-...", alt: "or CLAUDE_CODE_OAUTH_TOKEN" }], opencode: [{ key: "OPENAI_API_KEY", label: "OpenAI API Key", placeholder: "sk-..." }], codex: [{ key: "OPENAI_API_KEY", label: "OpenAI API Key", placeholder: "sk-..." }], gemini: [{ key: "GEMINI_API_KEY", label: "Gemini API Key", placeholder: "AIza..." }], factory: [{ key: "FACTORY_API_KEY", label: "Factory API Key", placeholder: "fk-..." }] };
|
|
428
439
|
const providerKeys = { sprites: [{ key: "SPRITE_TOKEN", label: "Sprites.dev Token", placeholder: "user/org/.../token" }] };
|
|
429
|
-
(backendKeys[
|
|
440
|
+
(backendKeys[onboardEngine] || []).forEach(f => fields.push(f));
|
|
430
441
|
(providerKeys[onboardProvider] || []).forEach(f => fields.push(f));
|
|
431
442
|
|
|
432
443
|
const fieldHtml = fields.map((f, i) => \`
|
|
@@ -449,7 +460,7 @@ function renderOnboarding() {
|
|
|
449
460
|
<p style="font-size:13px;color:var(--muted);margin-top:4px">Your agent and environment are set up.</p>
|
|
450
461
|
<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px;margin-top:16px;width:100%;font-size:13px;display:flex;flex-direction:column;gap:6px">
|
|
451
462
|
<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Agent</span><span style="color:var(--heading)">\${esc(agents[0]?.name || "")}</span></div>
|
|
452
|
-
<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">
|
|
463
|
+
<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Engine</span><span style="font-family:var(--mono);font-size:11px">\${agents[0]?.engine || ""}</span></div>
|
|
453
464
|
<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Model</span><span style="font-family:var(--mono);font-size:11px">\${agents[0]?.model || ""}</span></div>
|
|
454
465
|
<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Environment</span><span style="color:var(--heading)">\${esc(environments[0]?.name || "")}</span></div>
|
|
455
466
|
\${onboardVaultId ? '<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Secrets</span><span style="color:var(--success)">✓ Vault configured</span></div>' : '<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Secrets</span><span style="color:var(--dim)">Using server .env</span></div>'}
|
|
@@ -467,7 +478,7 @@ function renderOnboarding() {
|
|
|
467
478
|
}
|
|
468
479
|
|
|
469
480
|
function updateObModels() {
|
|
470
|
-
const backend = document.getElementById("
|
|
481
|
+
const backend = document.getElementById("obEngine")?.value || "claude";
|
|
471
482
|
const models = MODELS[backend] || MODELS.claude;
|
|
472
483
|
const el = document.getElementById("obModel");
|
|
473
484
|
if (el) el.innerHTML = models.map(m => \`<option value="\${m}">\${m}</option>\`).join("");
|
|
@@ -475,16 +486,16 @@ function updateObModels() {
|
|
|
475
486
|
|
|
476
487
|
async function onboardCreateAgent() {
|
|
477
488
|
const name = document.getElementById("obAgentName")?.value?.trim();
|
|
478
|
-
const backend = document.getElementById("
|
|
489
|
+
const backend = document.getElementById("obEngine")?.value;
|
|
479
490
|
const model = document.getElementById("obModel")?.value;
|
|
480
491
|
if (!name) return;
|
|
481
492
|
try {
|
|
482
|
-
const body = { name, model, backend };
|
|
493
|
+
const body = { name, model, engine: backend };
|
|
483
494
|
if (backend === "claude") body.tools = [{ type: "agent_toolset_20260401" }];
|
|
484
495
|
await api("/v1/agents", { method: "POST", body: JSON.stringify(body) });
|
|
485
496
|
const a = await api("/v1/agents?limit=50");
|
|
486
497
|
agents = a.data || [];
|
|
487
|
-
|
|
498
|
+
onboardEngine = backend;
|
|
488
499
|
onboardingStep = 1; // → environment step
|
|
489
500
|
renderOnboarding();
|
|
490
501
|
showToast("Agent created");
|
|
@@ -814,7 +825,7 @@ function renderAgents() {
|
|
|
814
825
|
if (!agents.length) { el.innerHTML = '<p style="color:var(--dim);font-size:12px;padding:8px">No agents yet</p>'; return; }
|
|
815
826
|
el.innerHTML = agents.map((a) => \`
|
|
816
827
|
<div class="card-item">
|
|
817
|
-
<div><div class="name">\${esc(a.name)}</div><div class="detail">\${esc(a.model)} / \${a.
|
|
828
|
+
<div><div class="name">\${esc(a.name)}</div><div class="detail">\${esc(a.model)} / \${a.engine}</div></div>
|
|
818
829
|
<div style="display:flex;gap:4px">
|
|
819
830
|
<button class="btn btn-sm btn-secondary" onclick="showAgentConfig('\${esc(a.id)}')">Config</button>
|
|
820
831
|
<button class="btn btn-sm btn-danger" onclick="deleteAgent('\${esc(a.id)}')">Delete</button>
|
|
@@ -890,8 +901,8 @@ function showCreateAgentModal() {
|
|
|
890
901
|
<div class="modal">
|
|
891
902
|
<h2>Create Agent</h2>
|
|
892
903
|
<div class="form-group"><label class="form-label">Name</label><input class="form-input" id="agentName" value="Coder" /></div>
|
|
893
|
-
<div class="form-group"><label class="form-label">
|
|
894
|
-
<select class="form-select" id="
|
|
904
|
+
<div class="form-group"><label class="form-label">Engine</label>
|
|
905
|
+
<select class="form-select" id="agentEngine" onchange="updateModelOptions()">
|
|
895
906
|
<option value="claude">Claude</option><option value="opencode">OpenCode</option><option value="codex">Codex</option><option value="gemini">Gemini</option><option value="factory">Factory</option>
|
|
896
907
|
</select>
|
|
897
908
|
</div>
|
|
@@ -906,18 +917,18 @@ function showCreateAgentModal() {
|
|
|
906
917
|
}
|
|
907
918
|
|
|
908
919
|
function updateModelOptions() {
|
|
909
|
-
const backend = document.getElementById("
|
|
920
|
+
const backend = document.getElementById("agentEngine").value;
|
|
910
921
|
const models = MODELS[backend] || MODELS.claude;
|
|
911
922
|
document.getElementById("agentModel").innerHTML = models.map((m) => \`<option value="\${m}">\${m}</option>\`).join("");
|
|
912
923
|
}
|
|
913
924
|
|
|
914
925
|
async function createAgent() {
|
|
915
926
|
const name = document.getElementById("agentName").value.trim();
|
|
916
|
-
const backend = document.getElementById("
|
|
927
|
+
const backend = document.getElementById("agentEngine").value;
|
|
917
928
|
const model = document.getElementById("agentModel").value;
|
|
918
929
|
if (!name) return;
|
|
919
930
|
try {
|
|
920
|
-
const body = { name, model, backend };
|
|
931
|
+
const body = { name, model, engine: backend };
|
|
921
932
|
if (backend === "claude") body.tools = [{ type: "agent_toolset_20260401" }];
|
|
922
933
|
await api("/v1/agents", { method: "POST", body: JSON.stringify(body) });
|
|
923
934
|
closeModal(); loadResources(); showToast("Agent created");
|
|
@@ -936,14 +947,20 @@ function showCreateEnvModal() {
|
|
|
936
947
|
<div class="form-group"><label class="form-label">Name</label><input class="form-input" id="envName" value="dev" style="width:100%" /></div>
|
|
937
948
|
<div class="form-group"><label class="form-label">Provider</label>
|
|
938
949
|
<select class="form-select" id="envProvider" onchange="toggleEnvToken()" style="width:100%">
|
|
939
|
-
<
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
<
|
|
946
|
-
|
|
950
|
+
<optgroup label="Local">
|
|
951
|
+
<option value="docker">Docker</option>
|
|
952
|
+
<option value="apple-container">Apple Container (macOS 26+)</option>
|
|
953
|
+
<option value="apple-firecracker">AgentStep Firecracker microVM (M3+)</option>
|
|
954
|
+
<option value="podman">Podman</option>
|
|
955
|
+
</optgroup>
|
|
956
|
+
<optgroup label="Cloud">
|
|
957
|
+
<option value="sprites">sprites.dev</option>
|
|
958
|
+
<option value="e2b">E2B</option>
|
|
959
|
+
<option value="fly">Fly.io</option>
|
|
960
|
+
<option value="vercel">Vercel</option>
|
|
961
|
+
<option value="daytona">Daytona</option>
|
|
962
|
+
<option value="modal">Modal</option>
|
|
963
|
+
</optgroup>
|
|
947
964
|
</select>
|
|
948
965
|
</div>
|
|
949
966
|
<div class="form-group" id="envTokenGroup" style="display:none">
|
|
@@ -1262,7 +1279,7 @@ function renderMarkdown(text) {
|
|
|
1262
1279
|
</body>
|
|
1263
1280
|
</html>
|
|
1264
1281
|
`;
|
|
1265
|
-
const UI_VERSION = "
|
|
1282
|
+
const UI_VERSION = "5c61307a";
|
|
1266
1283
|
|
|
1267
1284
|
export async function handleGetUI(opts?: { apiKey?: string }): Promise<Response> {
|
|
1268
1285
|
const inject = opts?.apiKey
|
package/src/openapi/schemas.ts
CHANGED
|
@@ -117,9 +117,9 @@ export const AgentSchema = registry.register(
|
|
|
117
117
|
system: z.string().nullable(),
|
|
118
118
|
tools: z.array(ToolConfigSchema),
|
|
119
119
|
mcp_servers: z.record(McpServerConfigSchema),
|
|
120
|
-
|
|
120
|
+
engine: z.enum(["claude", "opencode", "codex", "anthropic", "gemini", "factory"]).openapi({
|
|
121
121
|
description:
|
|
122
|
-
"Which
|
|
122
|
+
"Which agent harness powers this agent. `claude` drives `claude -p`; `opencode` drives sst/opencode-ai's `opencode run`; `gemini` drives Google's `gemini -p`; `factory` drives Factory's `droid exec`. Immutable after agent creation.",
|
|
123
123
|
}),
|
|
124
124
|
webhook_url: z.string().nullable().openapi({
|
|
125
125
|
description: "URL to POST webhook notifications to. Best-effort delivery with 5s timeout.",
|
|
@@ -147,9 +147,9 @@ export const CreateAgentRequestSchema = registry.register(
|
|
|
147
147
|
system: z.string().nullish().openapi({ example: "You are a helpful assistant." }),
|
|
148
148
|
tools: z.array(ToolConfigSchema).optional(),
|
|
149
149
|
mcp_servers: z.record(McpServerConfigSchema).optional(),
|
|
150
|
-
|
|
150
|
+
engine: z.enum(["claude", "opencode", "codex", "anthropic", "gemini", "factory"]).optional().openapi({
|
|
151
151
|
description:
|
|
152
|
-
"
|
|
152
|
+
"Agent harness. Defaults to `claude`. Opencode agents must set `model` to a `provider/model` string (e.g. `anthropic/claude-sonnet-4-6`) and must NOT declare `tools` — opencode manages its tool surface internally. Gemini agents require GEMINI_API_KEY. Factory agents require FACTORY_API_KEY.",
|
|
153
153
|
example: "claude",
|
|
154
154
|
}),
|
|
155
155
|
webhook_url: z.string().url().optional().openapi({
|
|
@@ -218,9 +218,9 @@ export const EnvironmentConfigSchema = registry.register(
|
|
|
218
218
|
"EnvironmentConfig",
|
|
219
219
|
z.object({
|
|
220
220
|
type: z.literal("cloud"),
|
|
221
|
-
provider: z.enum(["sprites", "docker", "apple", "podman", "e2b", "vercel", "daytona", "fly", "modal"]).optional().openapi({
|
|
221
|
+
provider: z.enum(["sprites", "docker", "apple-container", "apple-firecracker", "podman", "e2b", "vercel", "daytona", "fly", "modal"]).optional().openapi({
|
|
222
222
|
description:
|
|
223
|
-
"Container provider for this environment. `sprites` uses sprites.dev cloud containers (default); `docker` uses local Docker containers; `apple` uses Apple Containers on macOS 26+ (Apple Silicon only); `podman` uses Podman containers; `e2b` uses E2B cloud sandboxes; `vercel` uses Vercel Sandboxes; `daytona` uses Daytona workspaces; `fly` uses Fly.io Machines; `modal` uses Modal sandboxes.",
|
|
223
|
+
"Container provider for this environment. `sprites` uses sprites.dev cloud containers (default); `docker` uses local Docker containers; `apple-container` uses Apple Containers on macOS 26+ (Apple Silicon only); `apple-firecracker` uses AgentStep Firecracker microVMs (macOS, M3+ Apple Silicon); `podman` uses Podman containers; `e2b` uses E2B cloud sandboxes; `vercel` uses Vercel Sandboxes; `daytona` uses Daytona workspaces; `fly` uses Fly.io Machines; `modal` uses Modal sandboxes.",
|
|
224
224
|
}),
|
|
225
225
|
packages: EnvironmentPackages.optional(),
|
|
226
226
|
networking: EnvironmentNetworking.optional(),
|
|
@@ -165,7 +165,7 @@ function containerExecStreaming(
|
|
|
165
165
|
// ---------------------------------------------------------------------------
|
|
166
166
|
|
|
167
167
|
export const appleProvider: ContainerProvider = {
|
|
168
|
-
name: "apple",
|
|
168
|
+
name: "apple-container",
|
|
169
169
|
stripControlChars: false,
|
|
170
170
|
|
|
171
171
|
async checkAvailability() {
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mvm (AgentStep Machines) provider.
|
|
3
|
+
*
|
|
4
|
+
* Runs CLI backends inside hardware-isolated Firecracker microVMs via mvm.
|
|
5
|
+
* Each session gets its own VM with a separate Linux kernel — stronger
|
|
6
|
+
* isolation than Docker (shared kernel) or Apple Containers (shared VZ).
|
|
7
|
+
*
|
|
8
|
+
* Requires mvm to be installed: `brew install agentstep/tap/mvm` or
|
|
9
|
+
* `npm install -g @agentstep/mvm`.
|
|
10
|
+
*
|
|
11
|
+
* VM lifecycle:
|
|
12
|
+
* create → mvm start {name}
|
|
13
|
+
* exec → mvm exec {name} -- {argv}
|
|
14
|
+
* delete → mvm delete {name} --force
|
|
15
|
+
*
|
|
16
|
+
* Features beyond Docker:
|
|
17
|
+
* - Hardware isolation via KVM (separate kernel per VM)
|
|
18
|
+
* - Network sandboxing (--net-policy deny)
|
|
19
|
+
* - Pause/resume with full memory state
|
|
20
|
+
* - Pre-installed AI agents (Claude Code, Codex, Gemini, OpenCode)
|
|
21
|
+
*/
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { Readable } from "node:stream";
|
|
24
|
+
import type { ContainerProvider, ExecOptions, ExecSession } from "./types";
|
|
25
|
+
|
|
26
|
+
import { execSync } from "node:child_process";
|
|
27
|
+
|
|
28
|
+
// Use native mvm if available, otherwise npx
|
|
29
|
+
let CLI = "mvm";
|
|
30
|
+
try {
|
|
31
|
+
execSync("which mvm", { stdio: "ignore" });
|
|
32
|
+
} catch {
|
|
33
|
+
CLI = "npx";
|
|
34
|
+
}
|
|
35
|
+
const CLI_ARGS: string[] = CLI === "npx" ? ["--yes", "@agentstep/mvm"] : [];
|
|
36
|
+
|
|
37
|
+
function mvmRun(
|
|
38
|
+
args: string[],
|
|
39
|
+
opts?: { stdin?: string; timeoutMs?: number },
|
|
40
|
+
): Promise<string> {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const proc = spawn(CLI, [...CLI_ARGS, ...args], { stdio: ["pipe", "pipe", "pipe"] });
|
|
43
|
+
|
|
44
|
+
let stdout = "";
|
|
45
|
+
let stderr = "";
|
|
46
|
+
proc.stdout?.on("data", (buf: Buffer) => { stdout += buf.toString(); });
|
|
47
|
+
proc.stderr?.on("data", (buf: Buffer) => { stderr += buf.toString(); });
|
|
48
|
+
|
|
49
|
+
if (opts?.stdin) {
|
|
50
|
+
proc.stdin?.write(opts.stdin);
|
|
51
|
+
}
|
|
52
|
+
proc.stdin?.end();
|
|
53
|
+
|
|
54
|
+
const timer = opts?.timeoutMs
|
|
55
|
+
? setTimeout(() => {
|
|
56
|
+
proc.kill("SIGKILL");
|
|
57
|
+
reject(new Error(`mvm command timed out after ${opts.timeoutMs}ms`));
|
|
58
|
+
}, opts.timeoutMs)
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
proc.on("close", (code) => {
|
|
62
|
+
if (timer) clearTimeout(timer);
|
|
63
|
+
if (code !== 0) {
|
|
64
|
+
reject(new Error(`mvm ${args[0]} failed (${code}): ${stderr.trim()}`));
|
|
65
|
+
} else {
|
|
66
|
+
resolve(stdout);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
proc.on("error", (err) => {
|
|
71
|
+
if (timer) clearTimeout(timer);
|
|
72
|
+
reject(err);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function mvmExecOneShot(
|
|
78
|
+
vmName: string,
|
|
79
|
+
argv: string[],
|
|
80
|
+
stdin?: string,
|
|
81
|
+
timeoutMs?: number,
|
|
82
|
+
): Promise<{ stdout: string; stderr: string; exit_code: number }> {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const proc = spawn(CLI, [...CLI_ARGS, "exec", vmName, "--", ...argv], {
|
|
85
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let stdout = "";
|
|
89
|
+
let stderr = "";
|
|
90
|
+
proc.stdout?.on("data", (buf: Buffer) => { stdout += buf.toString(); });
|
|
91
|
+
proc.stderr?.on("data", (buf: Buffer) => { stderr += buf.toString(); });
|
|
92
|
+
|
|
93
|
+
if (stdin) proc.stdin?.write(stdin);
|
|
94
|
+
proc.stdin?.end();
|
|
95
|
+
|
|
96
|
+
const timer = timeoutMs
|
|
97
|
+
? setTimeout(() => {
|
|
98
|
+
proc.kill("SIGKILL");
|
|
99
|
+
reject(new Error(`mvm exec timed out after ${timeoutMs}ms`));
|
|
100
|
+
}, timeoutMs)
|
|
101
|
+
: null;
|
|
102
|
+
|
|
103
|
+
proc.on("close", (code) => {
|
|
104
|
+
if (timer) clearTimeout(timer);
|
|
105
|
+
resolve({ stdout, stderr, exit_code: code ?? 1 });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
proc.on("error", (err) => {
|
|
109
|
+
if (timer) clearTimeout(timer);
|
|
110
|
+
reject(err);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function mvmExecStreaming(vmName: string, opts: ExecOptions): ExecSession {
|
|
116
|
+
const proc = spawn(CLI, [...CLI_ARGS, "exec", vmName, "--", ...opts.argv], {
|
|
117
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (opts.stdin) proc.stdin?.write(opts.stdin);
|
|
121
|
+
proc.stdin?.end();
|
|
122
|
+
|
|
123
|
+
const stdout = Readable.toWeb(proc.stdout!) as ReadableStream<Uint8Array>;
|
|
124
|
+
|
|
125
|
+
let exitResolve: (v: { code: number }) => void;
|
|
126
|
+
let exitReject: (e: unknown) => void;
|
|
127
|
+
const exit = new Promise<{ code: number }>((res, rej) => {
|
|
128
|
+
exitResolve = res;
|
|
129
|
+
exitReject = rej;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
proc.on("close", (code) => exitResolve({ code: code ?? 0 }));
|
|
133
|
+
proc.on("error", (err) => exitReject(err));
|
|
134
|
+
|
|
135
|
+
let timer: NodeJS.Timeout | null = null;
|
|
136
|
+
if (opts.timeoutMs) {
|
|
137
|
+
timer = setTimeout(() => {
|
|
138
|
+
proc.kill("SIGKILL");
|
|
139
|
+
exitReject(new Error(`mvm exec timed out after ${opts.timeoutMs}ms`));
|
|
140
|
+
}, opts.timeoutMs);
|
|
141
|
+
}
|
|
142
|
+
exit.finally(() => { if (timer) clearTimeout(timer); });
|
|
143
|
+
|
|
144
|
+
if (opts.signal) {
|
|
145
|
+
if (opts.signal.aborted) {
|
|
146
|
+
proc.kill("SIGTERM");
|
|
147
|
+
} else {
|
|
148
|
+
opts.signal.addEventListener("abort", () => {
|
|
149
|
+
proc.kill("SIGTERM");
|
|
150
|
+
setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 3000);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
stdout,
|
|
157
|
+
exit,
|
|
158
|
+
async kill() {
|
|
159
|
+
proc.kill("SIGTERM");
|
|
160
|
+
setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 3000);
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const NET_POLICY = process.env.MVM_NET_POLICY ?? "deny";
|
|
166
|
+
|
|
167
|
+
export const mvmProvider: ContainerProvider = {
|
|
168
|
+
name: "apple-firecracker",
|
|
169
|
+
stripControlChars: false,
|
|
170
|
+
|
|
171
|
+
async checkAvailability() {
|
|
172
|
+
try {
|
|
173
|
+
await mvmRun(["version"], { timeoutMs: 30_000 });
|
|
174
|
+
return { available: true };
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
if (msg.includes("ENOENT")) {
|
|
178
|
+
return { available: false, message: "mvm is not installed. Install via: npm install -g @agentstep/mvm" };
|
|
179
|
+
}
|
|
180
|
+
return { available: false, message: `mvm is not available: ${msg}` };
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async create({ name }) {
|
|
185
|
+
// Clean up any existing VM with this name
|
|
186
|
+
await mvmRun(["delete", name, "--force"]).catch(() => {});
|
|
187
|
+
await mvmRun(["start", name, "--net-policy", NET_POLICY], { timeoutMs: 180_000 });
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
async delete(name) {
|
|
191
|
+
await mvmRun(["delete", name, "--force"]).catch(() => {});
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async list(opts) {
|
|
195
|
+
try {
|
|
196
|
+
const out = await mvmRun(["list", "--json"]);
|
|
197
|
+
const vms = JSON.parse(out) as { name: string }[];
|
|
198
|
+
const prefix = opts?.prefix ?? "ca-sess-";
|
|
199
|
+
return vms.filter((v) => v.name.startsWith(prefix)).map((v) => ({ name: v.name }));
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async exec(name, argv, opts) {
|
|
206
|
+
return mvmExecOneShot(name, argv, opts?.stdin, opts?.timeoutMs);
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
startExec(name, opts) {
|
|
210
|
+
return Promise.resolve(mvmExecStreaming(name, opts));
|
|
211
|
+
},
|
|
212
|
+
};
|
|
@@ -10,7 +10,8 @@ import type { ContainerProvider, ProviderName } from "./types";
|
|
|
10
10
|
const PROVIDERS: Record<ProviderName, () => Promise<ContainerProvider>> = {
|
|
11
11
|
sprites: async () => (await import("./sprites")).spritesProvider,
|
|
12
12
|
docker: async () => (await import("./docker")).dockerProvider,
|
|
13
|
-
apple: async () => (await import("./apple")).appleProvider,
|
|
13
|
+
"apple-container": async () => (await import("./apple-container")).appleProvider,
|
|
14
|
+
"apple-firecracker": async () => (await import("./apple-firecracker")).mvmProvider,
|
|
14
15
|
podman: async () => (await import("./podman")).podmanProvider,
|
|
15
16
|
e2b: async () => (await import("./e2b")).e2bProvider,
|
|
16
17
|
vercel: async () => (await import("./vercel")).vercelProvider,
|
package/src/providers/types.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* is selected per-environment via `EnvironmentConfig.provider`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export type ProviderName = "sprites" | "docker" | "apple" | "podman" | "e2b" | "vercel" | "daytona" | "fly" | "modal";
|
|
10
|
+
export type ProviderName = "sprites" | "docker" | "apple-container" | "apple-firecracker" | "podman" | "e2b" | "vercel" | "daytona" | "fly" | "modal";
|
|
11
11
|
|
|
12
12
|
export interface ExecOptions {
|
|
13
13
|
argv: string[];
|
package/src/sessions/driver.ts
CHANGED
|
@@ -79,7 +79,7 @@ export async function runTurn(
|
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const backend = resolveBackend(agent.
|
|
82
|
+
const backend = resolveBackend(agent.engine);
|
|
83
83
|
|
|
84
84
|
// Belt-and-braces runtime validation. Config may have changed since the
|
|
85
85
|
// agent was created (env vars cleared, settings table mutated). Fail fast
|
|
@@ -275,7 +275,7 @@ export async function runTurn(
|
|
|
275
275
|
|
|
276
276
|
// Tool bridge: if this is a custom tool result re-entry on claude backend,
|
|
277
277
|
// write response.json and remove the pending sentinel before --resume.
|
|
278
|
-
if (agent.
|
|
278
|
+
if (agent.engine === "claude" && toolResults.length > 0) {
|
|
279
279
|
const { TOOL_BRIDGE_RESPONSE_PATH, TOOL_BRIDGE_PENDING_PATH } = await import("../backends/claude/tool-bridge");
|
|
280
280
|
const spriteName = getSessionRow(sessionId)?.sprite_name;
|
|
281
281
|
if (spriteName) {
|
package/src/sprite/lifecycle.ts
CHANGED
|
@@ -74,7 +74,7 @@ export async function acquireForFirstTurn(sessionId: string): Promise<string> {
|
|
|
74
74
|
if (!agent) {
|
|
75
75
|
throw new ApiError(404, "not_found_error", "agent not found for session");
|
|
76
76
|
}
|
|
77
|
-
const backend = resolveBackend(agent.
|
|
77
|
+
const backend = resolveBackend(agent.engine);
|
|
78
78
|
|
|
79
79
|
// Resolve the container provider from the environment config.
|
|
80
80
|
// Defaults to "sprites" for backward compatibility.
|
|
@@ -110,7 +110,7 @@ export async function acquireForFirstTurn(sessionId: string): Promise<string> {
|
|
|
110
110
|
await backend.prepareOnSprite(name, provider);
|
|
111
111
|
|
|
112
112
|
// Install custom tool bridge if the agent has custom tools or threads_enabled (claude backend only)
|
|
113
|
-
if (agent.
|
|
113
|
+
if (agent.engine === "claude") {
|
|
114
114
|
const customTools = agent.tools.filter(
|
|
115
115
|
(t): t is import("../types").CustomTool => t.type === "custom",
|
|
116
116
|
);
|
package/src/types.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface McpServerConfig {
|
|
|
57
57
|
* the backend registry.
|
|
58
58
|
*/
|
|
59
59
|
export type BackendName = "claude" | "opencode" | "codex" | "anthropic" | "gemini" | "factory";
|
|
60
|
+
/** API-facing alias for BackendName. */
|
|
61
|
+
export type EngineName = BackendName;
|
|
60
62
|
|
|
61
63
|
export interface AgentRow {
|
|
62
64
|
id: string;
|
|
@@ -91,7 +93,7 @@ export interface Agent {
|
|
|
91
93
|
system: string | null;
|
|
92
94
|
tools: ToolConfig[];
|
|
93
95
|
mcp_servers: Record<string, McpServerConfig>;
|
|
94
|
-
|
|
96
|
+
engine: EngineName;
|
|
95
97
|
webhook_url: string | null;
|
|
96
98
|
webhook_events: string[];
|
|
97
99
|
threads_enabled: boolean;
|
|
@@ -109,7 +111,7 @@ export type EnvironmentState = "preparing" | "ready" | "failed";
|
|
|
109
111
|
|
|
110
112
|
export interface EnvironmentConfig {
|
|
111
113
|
type: "cloud";
|
|
112
|
-
provider?: "sprites" | "docker" | "apple" | "podman" | "e2b" | "vercel" | "daytona" | "fly" | "modal";
|
|
114
|
+
provider?: "sprites" | "docker" | "apple-container" | "apple-firecracker" | "podman" | "e2b" | "vercel" | "daytona" | "fly" | "modal";
|
|
113
115
|
packages?: {
|
|
114
116
|
apt?: string[];
|
|
115
117
|
cargo?: string[];
|