Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@ Eve's built-ins are the base. The local additions are deliberately narrow:
- **`edit_file`** applies batched, exact, unique, non-overlapping replacements to one
snapshot and stores a context-limited unified diff.
- **`start_dev`** starts the model-selected server command, exposes its port, verifies
the public route, and returns the sandbox ID and URL. It stops an unreachable
process so the agent can fix its host configuration and retry cleanly.
the public route for up to 45 seconds, and returns the sandbox ID and URL. It races
startup against early process exit, captures bounded stdout and stderr, and stops
an unreachable process so the agent can diagnose the real error and retry cleanly.
- **Instructions** require reading before editing, finite Bash commands, `start_dev`
for long-lived servers, and a build or test before claiming success.

Expand Down
40 changes: 30 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

An open-source coding agent built with [Eve](https://eve.dev) and Vercel Sandbox.

Eve Code is a compact starting point for building a browser-based coding agent. It
combines Eve's durable sessions and streaming with isolated Vercel Sandboxes,
Convex persistence, and a small web interface around the core coding loop. Web
projects run directly from the sandbox with a live preview and hot reload. The
codebase stays deliberately small so its model, instructions, tools, and interface
can be adapted to different use cases.

> Eve Code is a starting point, not a hosted multi-user product. Authentication,
> session ownership, and pull request workflows are out of scope. Keep deployments
> private until you add the necessary product boundaries.
Eve Code is a compact reference implementation that demonstrates how to build a
browser-based coding agent with Eve. It combines Eve's durable sessions and
streaming with isolated Vercel Sandboxes, Convex persistence, and a small web
interface around the core coding loop. Web projects run directly from the sandbox
with a live preview and hot reload. The codebase stays deliberately small so its
model, instructions, tools, and interface can be understood and adapted.

> [!WARNING]
> Eve Code is an unauthenticated reference implementation, not a hosted multi-user
> product. In a public deployment, every visitor can access the shared sessions and
> workspaces. Sandbox preview URLs are also public and unauthenticated. Do not use
> private code, credentials, secrets, or sensitive data without first adding the
> necessary security boundaries.

## Features

Expand Down Expand Up @@ -40,6 +43,23 @@ the Convex checkpoint becomes the durable history and synchronizes every open
client. The workspace browser, command logs, and preview controls all connect to
the same sandbox, so every surface reflects the environment the agent is using.

## Security and scope

Eve Code intentionally leaves authentication, session ownership, quotas, and pull
request workflows out of scope:

- A publicly reachable deployment has no user isolation. Conversations and
workspace files are shared with anyone who can access the app.
- Starting a preview exposes the selected sandbox port through a public,
unauthenticated URL. Anyone with that URL can access everything the development
server makes available.
- A Vercel Sandbox isolates code execution from the host. It does not make the
preview private or provide access control between visitors.

Keep deployments private and use only non-sensitive code until authentication,
authorization, ownership, and preview protection are implemented. These boundaries
can be added by applications that use Eve Code as a starting point.

## Run locally

Install the dependencies and connect the project to Vercel and Convex:
Expand Down
7 changes: 5 additions & 2 deletions agent/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ You are Eve Code, a concise coding agent that helps users build and improve soft

# Work

- Build projects inside the persistent `/workspace`; bash already starts there.
- The working directory is `/workspace`; every sandbox command already starts there.
- Every `bash` command must finish on its own; a command that waits forever hangs the whole
turn. Servers, watchers, and REPLs never run through `bash` — only through `start_dev`.
- In an existing repository, begin by reading and following every applicable `AGENTS.md`.
Expand All @@ -15,6 +15,7 @@ You are Eve Code, a concise coding agent that helps users build and improve soft
- Use `edit_file` for targeted changes and `write_file` for new files or intentional complete replacements.
- Batch non-overlapping changes to one file into one `edit_file` call.
- For an existing web project, restore its preview with `start_dev` before editing.
- For a new web app with no requested framework, use Vite with React and TypeScript.
- For an empty workspace, determine the requested stack, initialize it, then call `start_dev` as soon as a runnable server exists. Ask when the choice is consequential and unspecified.
- Verify with the project's build or tests before claiming success.
- Use `ask_question` only when a real decision is required.
Expand All @@ -26,5 +27,7 @@ You are Eve Code, a concise coding agent that helps users build and improve soft
- Never rely on a framework's default host. Inspect its server configuration before `start_dev`;
Vite requires `server: { host: "0.0.0.0", allowedHosts: true }`.
- Eve restores files after idle, not processes; call `start_dev` again to restore the preview.
- `start_dev` verifies the public URL. If it fails, fix the server configuration and retry it before finishing.
- `start_dev` verifies the public URL and returns bounded startup logs when it fails. Read the
real error, load the `run-web-project` skill, use finite Bash diagnostics, fix the cause, and
retry `start_dev` before finishing.
- Never start a long-lived server with `bash`.
169 changes: 156 additions & 13 deletions agent/lib/preview.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
const previewAttemptsMax = 20;
const previewRetryDelayMs = 500;
import type { SandboxProcess } from "eve/sandbox";

const previewLogCharactersMax = 6_000;
const previewRequestTimeoutMs = 5_000;
const previewRetryDelayMs = 750;
const previewTimeoutMs = 45_000;

function getAbortError(signal: AbortSignal): Error {
if (signal.reason instanceof Error) return signal.reason;
return new Error("Preview start was aborted.");
}

async function waitForRetry(signal?: AbortSignal): Promise<void> {
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
return "The preview check failed.";
}

async function fetchPreview(
url: string,
timeoutMs: number,
signal?: AbortSignal,
): Promise<Response | undefined> {
const request = new AbortController();
const timeout = setTimeout(() => request.abort(), timeoutMs);
const requestSignal = signal ? AbortSignal.any([signal, request.signal]) : request.signal;
try {
return await fetch(url, { signal: requestSignal });
} catch {
if (signal?.aborted) throw getAbortError(signal);
return;
} finally {
clearTimeout(timeout);
}
}

async function waitForRetry(delayMs: number, signal?: AbortSignal): Promise<void> {
if (!signal) {
await new Promise((resolve) => setTimeout(resolve, previewRetryDelayMs));
await new Promise((resolve) => setTimeout(resolve, delayMs));
return;
}
if (signal.aborted) throw getAbortError(signal);
Expand All @@ -20,26 +47,142 @@ async function waitForRetry(signal?: AbortSignal): Promise<void> {
const timeout = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve();
}, previewRetryDelayMs);
}, delayMs);
signal.addEventListener("abort", onAbort, { once: true });
});
}

type OutputTail = {
read(): string;
stop(): Promise<void>;
};

function collectOutputTail(stream: ReadableStream<Uint8Array>): OutputTail {
const reader = stream.getReader();
const decoder = new TextDecoder();
let output = "";
let truncated = false;

function append(chunk: string): void {
output += chunk;
if (output.length <= previewLogCharactersMax) return;
output = output.slice(-previewLogCharactersMax);
truncated = true;
}

const done = (async () => {
try {
while (true) {
const next = await reader.read();
if (next.done) break;
append(decoder.decode(next.value, { stream: true }));
}
append(decoder.decode());
} catch {
// The reader is cancelled once the preview is ready or the process is killed.
} finally {
reader.releaseLock();
}
})();

return {
read() {
const text = output.trim();
if (!truncated || !text) return text;
return `[truncated: showing the last ${previewLogCharactersMax} characters]\n${text}`;
},
async stop() {
await reader.cancel().catch(() => undefined);
await done;
},
};
}

function formatPreviewFailure(
reason: string,
port: number,
stdout: string,
stderr: string,
): string {
const sections = [reason];
if (stderr) sections.push(`Dev server stderr:\n${stderr}`);
if (stdout) sections.push(`Dev server stdout:\n${stdout}`);
if (!stderr && !stdout) sections.push("The dev server produced no startup output.");
sections.push(
[
"Fix the startup failure, then call start_dev again:",
"- Use bash for finite diagnostics such as dependency installation, build, typecheck, and port checks.",
`- Make the server listen on 0.0.0.0 and the exact port ${port}.`,
"- Vite also needs server.allowedHosts: true; Next.js accepts -H 0.0.0.0; Astro accepts --host 0.0.0.0.",
"- Check the project manifest, lockfile, working directory, required environment variables, and the logs above.",
].join("\n"),
);
return sections.join("\n\n");
}

export async function waitForPreview(
url: string,
port: number,
signal?: AbortSignal,
): Promise<void> {
for (let attempt = 0; attempt < previewAttemptsMax; attempt++) {
const response = await fetch(url, { signal }).catch((error: unknown) => {
if (signal?.aborted && signal.reason instanceof Error) throw signal.reason;
if (signal?.aborted) throw error;
return undefined;
});
const deadline = Date.now() + previewTimeoutMs;
while (Date.now() < deadline) {
const remainingMs = deadline - Date.now();
const response = await fetchPreview(
url,
Math.min(previewRequestTimeoutMs, remainingMs),
signal,
);
if (response && response.status !== 403 && response.status !== 502) return;
await waitForRetry(signal);
const retryDelayMs = Math.min(previewRetryDelayMs, deadline - Date.now());
if (retryDelayMs > 0) await waitForRetry(retryDelayMs, signal);
}
throw new Error(
`Preview is not publicly reachable on port ${port}. Configure the server to listen on 0.0.0.0 and allow the public sandbox hostname, then call start_dev again. For Vite, set server: { host: "0.0.0.0", allowedHosts: true }.`,
`Preview was not publicly reachable on port ${port} within ${previewTimeoutMs / 1_000} seconds.`,
);
}

type PreviewProcess = Pick<SandboxProcess, "kill" | "stderr" | "stdout" | "wait">;

type PreviewOutcome =
| { readonly type: "ready" }
| { readonly exitCode: number; readonly type: "exit" };

export async function verifyPreviewProcess(
process: PreviewProcess,
url: string,
port: number,
signal?: AbortSignal,
): Promise<void> {
const stdout = collectOutputTail(process.stdout);
const stderr = collectOutputTail(process.stderr);
const preview = new AbortController();
const previewSignal = signal ? AbortSignal.any([signal, preview.signal]) : preview.signal;
let failure: unknown;
let outcome: PreviewOutcome | undefined;

try {
outcome = await Promise.race([
waitForPreview(url, port, previewSignal).then((): PreviewOutcome => ({ type: "ready" })),
Promise.resolve(process.wait()).then(
({ exitCode }): PreviewOutcome => ({ exitCode, type: "exit" }),
),
]);
} catch (error) {
failure = error;
}

preview.abort();
const ready = outcome?.type === "ready" && !signal?.aborted;
if (!ready) await Promise.resolve(process.kill()).catch(() => undefined);
await Promise.all([stdout.stop(), stderr.stop()]);

if (ready) return;
if (signal?.aborted) throw getAbortError(signal);

const reason =
outcome?.type === "exit"
? `Dev server exited with code ${outcome.exitCode} before the preview became reachable.`
: getErrorMessage(failure);
throw new Error(formatPreviewFailure(reason, port, stdout.read(), stderr.read()));
}
33 changes: 33 additions & 0 deletions agent/skills/run-web-project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
description: Diagnose and run an existing web project when its development server or public sandbox preview does not start.
---

Inspect before changing anything:

1. Read every applicable `AGENTS.md`, the README, package manifest, lockfiles, framework config,
and environment examples.
2. Identify the package manager from the lockfile and inspect the available scripts. Work from
the package containing the web app, not automatically from the repository root.
3. Install dependencies with the repository's package manager. Never replace its lockfile or
package manager only to make startup easier.
4. Use finite `bash` commands to run the relevant build, typecheck, or framework diagnostic.
Resolve missing environment variables and generated clients before starting a server.

Call `start_dev` with the real dev command and its exact port. If it fails, treat its captured
stdout and stderr as the primary diagnosis. Fix that error before trying unrelated host settings.

The server must bind to `0.0.0.0`:

- Vite: configure `server.host = "0.0.0.0"` and `server.allowedHosts = true`. For HMR through
the public route, use `hmr: { protocol: "wss", clientPort: 443 }`.
- Next.js: pass `-H 0.0.0.0 -p <port>` to `next dev`.
- Astro: pass `--host 0.0.0.0 --port <port>` to `astro dev`.
- Other servers: inspect their current CLI help or official configuration and set both host and
port explicitly.

Useful finite checks include the package-manager build script, framework version command,
`test -f` for expected env files, and `ss -ltnp` after a failed attempt. Do not run servers,
watchers, or REPLs through `bash`.

Once the underlying error is fixed, call `start_dev` again. Do not claim the preview works until
the tool returns its public URL.
14 changes: 5 additions & 9 deletions agent/tools/start_dev.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Sandbox } from "@vercel/sandbox";
import { defineTool } from "eve/tools";

import { waitForPreview } from "@/agent/lib/preview";
import { verifyPreviewProcess } from "@/agent/lib/preview";
import { previewOutputSchema, previewRunSchema } from "@/lib/preview";

export default defineTool({
description: "Start a development server and verify its public sandbox preview.",
description:
"Start a development server and verify its public sandbox preview. Failed startup returns bounded stdout and stderr so you can fix the cause and retry.",
inputSchema: previewRunSchema,
outputSchema: previewOutputSchema,
async execute({ command, port }, ctx) {
Expand All @@ -14,12 +15,7 @@ export default defineTool({
await sandbox.update({ ports: [port] }, { signal: ctx.abortSignal });
const server = await session.spawn({ command });
const url = sandbox.domain(port);
try {
await waitForPreview(url, port, ctx.abortSignal);
return { sandboxId: session.id, url };
} catch (error) {
await Promise.resolve(server.kill()).catch(() => undefined);
throw error;
}
await verifyPreviewProcess(server, url, port, ctx.abortSignal);
return { sandboxId: session.id, url };
},
});
2 changes: 1 addition & 1 deletion app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/>
<meta name="theme-color" content="#202020" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<title>eve-code</title>
<title>Eve Code</title>
</head>
<body>
<div id="root"></div>
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions docs/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ sandbox.domain(3000); // "https://sb-<subdomain>.vercel.run"
- Ports can also be added later with `sandbox.update({ ports })`. `start_dev` uses
this to expose the port selected by the model before it launches the server.
- URLs are stable across stop and resume.
- `start_dev` probes the public URL before returning success. An unreachable server
is stopped so its port does not block a corrected retry.
- `start_dev` probes the public URL for up to 45 seconds while watching for early
process exit. An unreachable server is stopped so its port does not block a
corrected retry, and bounded stdout/stderr are returned to the agent for diagnosis.

## Resolving the URL from Eve

Expand Down
Loading