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
6 changes: 6 additions & 0 deletions cli/src/adapters/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { printOpenClawGatewayStreamEvent } from "@paperclipai/adapter-openclaw-g
import { processCLIAdapter } from "./process/index.js";
import { httpCLIAdapter } from "./http/index.js";

const emissoSandboxCLIAdapter: CLIAdapterModule = {
type: "emisso_sandbox",
formatStdoutEvent: printClaudeStreamEvent,
};

const claudeLocalCLIAdapter: CLIAdapterModule = {
type: "claude_local",
formatStdoutEvent: printClaudeStreamEvent,
Expand Down Expand Up @@ -55,6 +60,7 @@ const adaptersByType = new Map<string, CLIAdapterModule>(
openclawGatewayCLIAdapter,
processCLIAdapter,
httpCLIAdapter,
emissoSandboxCLIAdapter,
].map((a) => [a.type, a]),
);

Expand Down
93 changes: 93 additions & 0 deletions doc/guides/engineering-agent-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Engineering Agent Setup Guide

How to configure an engineering agent using the Emisso Sandbox adapter.

## Recommended Adapter Config

```json
{
"model": "claude-sonnet-4-6",
"vcpus": 2,
"timeoutSec": 180,
"maxTurns": 30,
"cloneDepth": 1,
"snapshotId": "",
"mcpServers": {
"github": {
"command": "mcp-server-github",
"args": ["--token", "$GITHUB_TOKEN"]
}
}
}
```

## Environment Variables

| Variable | Required | Purpose |
|---|---|---|
| `ANTHROPIC_API_KEY` | Yes | Claude API authentication |
| `GITHUB_TOKEN` | For private repos | Git clone authentication |
| `VERCEL_TOKEN` | Non-Vercel hosts | Vercel Sandbox authentication |
| `VERCEL_TEAM_ID` | Non-Vercel hosts | Vercel team for sandbox billing |
| `VERCEL_PROJECT_ID` | Optional | Vercel project scope |

## MCP Server Examples

### GitHub MCP Server

Gives the agent access to GitHub issues, PRs, and repository metadata:

```json
{
"github": {
"command": "mcp-server-github",
"args": ["--token", "$GITHUB_TOKEN"]
}
}
```

### Supabase MCP Server

Gives the agent access to query the database:

```json
{
"supabase": {
"command": "mcp-server-supabase",
"args": ["--url", "$SUPABASE_URL", "--key", "$SUPABASE_SERVICE_KEY"]
}
}
```

## Creating an Agent via UI

1. Go to **Agents** → **Create Agent**
2. Set adapter type to **Emisso Sandbox**
3. Configure the model (Sonnet 4.6 recommended for most tasks)
4. Set vCPUs to 2 (increase to 4-8 for complex tasks)
5. Set timeout to 180s (increase for longer-running tasks)
6. Add MCP servers as needed
7. Set the repo URL (or rely on workspace context)
8. Run the environment test to verify configuration

## Snapshots

For faster cold starts, create a snapshot with the CLI pre-installed:

1. Create a sandbox manually with `@vercel/sandbox`
2. Install Claude Code: `npm install -g @anthropic-ai/claude-code`
3. Create a snapshot: `sandbox.snapshot()`
4. Use the returned `snapshotId` in the agent config

This reduces sandbox startup from ~60s to ~5s.

## Workspace Strategy

The adapter resolves the repo URL in this order:

1. `adapterConfig.repoUrl` (explicit override)
2. `context.paperclipWorkspace.repoUrl` (from project workspace)
3. Falls back with an error if neither is set

For project-scoped agents, the workspace context is automatically provided
by Paperclip when assigning issues from a project with a configured workspace.
7 changes: 7 additions & 0 deletions packages/adapter-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,14 @@ export interface CreateConfigValues {
workspaceBranchTemplate?: string;
worktreeParentDir?: string;
runtimeServicesJson?: string;
mcpServersJson?: string;
maxTurnsPerRun: number;
maxTurns?: number;
heartbeatEnabled: boolean;
intervalSec: number;
// Emisso Sandbox fields
repoUrl?: string;
vcpus?: number;
timeoutSec?: number;
snapshotId?: string;
}
1 change: 1 addition & 0 deletions packages/adapters/claude-local/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Core fields:
- env (object, optional): KEY=VALUE environment variables
- workspaceStrategy (object, optional): execution workspace strategy; currently supports { type: "git_worktree", baseRef?, branchTemplate?, worktreeParentDir? }
- workspaceRuntime (object, optional): workspace runtime service intents; local host-managed services are realized before Claude starts and exposed back via context/env
- mcpServers (object, optional): MCP server configuration. Keys are server names, values are { command, args?, env? }. Written to a temp file and passed via --mcp-config.

Operational fields:
- timeoutSec (number, optional): run timeout in seconds
Expand Down
9 changes: 9 additions & 0 deletions packages/adapters/claude-local/src/server/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const billingType = resolveClaudeBillingType(effectiveEnv);
const skillsDir = await buildSkillsDir(config);

// Write MCP server config file when mcpServers is configured.
const mcpServers = parseObject(config.mcpServers);
let mcpConfigPath: string | null = null;
if (Object.keys(mcpServers).length > 0) {
mcpConfigPath = path.join(skillsDir, "mcp-config.json");
await fs.writeFile(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8");
}

// When instructionsFilePath is configured, create a combined temp file that
// includes both the file content and the path directive, so we only need
// --append-system-prompt-file (Claude CLI forbids using both flags together).
Expand Down Expand Up @@ -406,6 +414,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
args.push("--append-system-prompt-file", effectiveInstructionsFilePath);
}
args.push("--add-dir", skillsDir);
if (mcpConfigPath) args.push("--mcp-config", mcpConfigPath);
if (extraArgs.length > 0) args.push(...extraArgs);
return args;
};
Expand Down
37 changes: 37 additions & 0 deletions packages/adapters/claude-local/src/server/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,43 @@ export async function testEnvironment(
});
}

// Validate mcpServers structure if configured
const mcpServers = parseObject(config.mcpServers);
const mcpKeys = Object.keys(mcpServers);
if (mcpKeys.length > 0) {
let allValid = true;
for (const key of mcpKeys) {
const entry = parseObject(mcpServers[key]);
const hasCommand = isNonEmpty(entry.command);
const hasUrl = isNonEmpty(entry.url);
if (!hasCommand && !hasUrl) {
checks.push({
code: "claude_mcp_server_invalid",
level: "warn",
message: `MCP server "${key}" is missing both "command" and "url".`,
hint: "Each MCP server entry must have a \"command\" (for stdio transport) or \"url\" (for SSE transport).",
});
allValid = false;
}
if (entry.args !== undefined && !Array.isArray(entry.args)) {
checks.push({
code: "claude_mcp_server_args_invalid",
level: "warn",
message: `MCP server "${key}" has non-array "args".`,
hint: "\"args\" must be a string array.",
});
allValid = false;
}
}
if (allValid) {
checks.push({
code: "claude_mcp_servers_valid",
level: "info",
message: `${mcpKeys.length} MCP server(s) configured: ${mcpKeys.join(", ")}`,
});
}
}

const canRunProbe =
checks.every((check) => check.code !== "claude_cwd_invalid" && check.code !== "claude_command_unresolvable");
if (canRunProbe) {
Expand Down
2 changes: 2 additions & 0 deletions packages/adapters/claude-local/src/ui/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,7 @@ export function buildClaudeLocalConfig(v: CreateConfigValues): Record<string, un
}
if (v.command) ac.command = v.command;
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);
const mcpServers = parseJsonObject(v.mcpServersJson ?? "");
if (mcpServers) ac.mcpServers = mcpServers;
return ac;
}
1 change: 1 addition & 0 deletions packages/shared/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const AGENT_ADAPTER_TYPES = [
"cursor",
"openclaw_gateway",
"hermes_local",
"emisso_sandbox",
] as const;
export type AgentAdapterType = (typeof AGENT_ADAPTER_TYPES)[number];

Expand Down
Loading