@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "license": "Apache-2.0",
3
3
  "name": "@agentstep/agent-sdk",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "src/index.ts",
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
- backend: (ver.backend ?? "claude") as BackendName,
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.backend,
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),
@@ -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
- backend: z.enum(["claude", "opencode", "codex", "anthropic", "gemini", "factory"]).optional(),
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
- const backendName = parsed.data.backend ?? "claude";
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(`environment ${id} still has active sessions attached`);
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" });
@@ -32,7 +32,7 @@ let lastAppendedRole = "";
32
32
 
33
33
  let onboardingStep = 0;
34
34
  let onboardVaultId = null;
35
- let onboardBackend = "claude";
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 a backend and model to get started.</p>
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">Backend</label>
122
- <select class="form-select" id="obBackend" onchange="updateObModels()" style="width:100%">
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
- <option value="docker">Docker — Local containers (~3s)</option>
142
- <option value="sprites">sprites.dev — Cloud containers (~2s)</option>
143
- <option value="apple">Apple Containers macOS 26+ (~1s)</option>
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[onboardBackend] || []).forEach(f => fields.push(f));
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)">Backend</span><span style="font-family:var(--mono);font-size:11px">${agents[0]?.backend || ""}</span></div>
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("obBackend")?.value || "claude";
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("obBackend")?.value;
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
- onboardBackend = backend;
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.backend}</div></div>
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">Backend</label>
618
- <select class="form-select" id="agentBackend" onchange="updateModelOptions()">
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("agentBackend").value;
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("agentBackend").value;
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
- <option value="docker">Docker — Local containers</option>
664
- <option value="sprites">sprites.dev — Cloud containers</option>
665
- <option value="apple">Apple Containers macOS 26+</option>
666
- <option value="e2b">E2B AI sandboxes</option>
667
- <option value="vercel">Vercel Sandbox — Firecracker VMs</option>
668
- <option value="daytona">Daytona — Dev environments</option>
669
- <option value="fly">Fly.io — Global VMs</option>
670
- <option value="modal">Modal — Serverless containers</option>
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">
@@ -308,7 +308,7 @@ let lastAppendedRole = "";
308
308
 
309
309
  let onboardingStep = 0;
310
310
  let onboardVaultId = null;
311
- let onboardBackend = "claude";
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 a backend and model to get started.</p>
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">Backend</label>
398
- <select class="form-select" id="obBackend" onchange="updateObModels()" style="width:100%">
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
- <option value="docker">Docker — Local containers (~3s)</option>
418
- <option value="sprites">sprites.dev — Cloud containers (~2s)</option>
419
- <option value="apple">Apple Containers macOS 26+ (~1s)</option>
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[onboardBackend] || []).forEach(f => fields.push(f));
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)">Backend</span><span style="font-family:var(--mono);font-size:11px">\${agents[0]?.backend || ""}</span></div>
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("obBackend")?.value || "claude";
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("obBackend")?.value;
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
- onboardBackend = backend;
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.backend}</div></div>
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">Backend</label>
894
- <select class="form-select" id="agentBackend" onchange="updateModelOptions()">
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("agentBackend").value;
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("agentBackend").value;
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
- <option value="docker">Docker — Local containers</option>
940
- <option value="sprites">sprites.dev — Cloud containers</option>
941
- <option value="apple">Apple Containers macOS 26+</option>
942
- <option value="e2b">E2B AI sandboxes</option>
943
- <option value="vercel">Vercel Sandbox — Firecracker VMs</option>
944
- <option value="daytona">Daytona — Dev environments</option>
945
- <option value="fly">Fly.io — Global VMs</option>
946
- <option value="modal">Modal — Serverless containers</option>
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 = "77929c44";
1282
+ const UI_VERSION = "5c61307a";
1266
1283
 
1267
1284
  export async function handleGetUI(opts?: { apiKey?: string }): Promise<Response> {
1268
1285
  const inject = opts?.apiKey
@@ -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
- backend: z.enum(["claude", "opencode", "codex", "anthropic", "gemini", "factory"]).openapi({
120
+ engine: z.enum(["claude", "opencode", "codex", "anthropic", "gemini", "factory"]).openapi({
121
121
  description:
122
- "Which CLI engine 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.",
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
- backend: z.enum(["claude", "opencode", "codex", "anthropic", "gemini", "factory"]).optional().openapi({
150
+ engine: z.enum(["claude", "opencode", "codex", "anthropic", "gemini", "factory"]).optional().openapi({
151
151
  description:
152
- "Backend CLI engine. 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.",
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,
@@ -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[];
@@ -79,7 +79,7 @@ export async function runTurn(
79
79
  return;
80
80
  }
81
81
 
82
- const backend = resolveBackend(agent.backend);
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.backend === "claude" && toolResults.length > 0) {
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) {
@@ -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.backend);
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.backend === "claude") {
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
- backend: BackendName;
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[];