mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 22:03:52 +00:00
feat: dev env bootstrap, deferred sessions, and mock agent fallback
- Add dotenv loader for NODE_ENV=development with .env.development.example - Add factory self-hosting deployment docs - Defer agent session creation in handoff init (create on first prompt) - Fall back to mock agent when Claude/Codex credentials are missing - Replace stub mock agent launcher with real ACP JSON-RPC Node.js script - Fix React useEffect dependency array in org settings - Clean up tracked .context cache/temp files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e08d1b4dca
commit
3022bce2ad
317 changed files with 496 additions and 167352 deletions
|
|
@ -7,7 +7,6 @@ import {
|
|||
getOrCreateHistory,
|
||||
getOrCreateProject,
|
||||
getOrCreateSandboxInstance,
|
||||
getSandboxInstance,
|
||||
selfHandoff
|
||||
} from "../../handles.js";
|
||||
import { logActorWarning, resolveErrorMessage } from "../../logging.js";
|
||||
|
|
@ -15,7 +14,6 @@ import { handoff as handoffTable, handoffRuntime, handoffSandboxes } from "../db
|
|||
import {
|
||||
HANDOFF_ROW_ID,
|
||||
appendHistory,
|
||||
buildAgentPrompt,
|
||||
collectErrorMessages,
|
||||
resolveErrorDetail,
|
||||
setHandoffState
|
||||
|
|
@ -394,7 +392,7 @@ export async function initCreateSessionActivity(
|
|||
sandbox: any,
|
||||
sandboxInstanceReady: any
|
||||
): Promise<any> {
|
||||
await setHandoffState(loopCtx, "init_create_session", "creating agent session");
|
||||
await setHandoffState(loopCtx, "init_create_session", "deferring agent session creation");
|
||||
if (!sandboxInstanceReady.ok) {
|
||||
return {
|
||||
id: null,
|
||||
|
|
@ -403,20 +401,11 @@ export async function initCreateSessionActivity(
|
|||
} as const;
|
||||
}
|
||||
|
||||
const { config } = getActorRuntimeContext();
|
||||
const providerId = body?.providerId ?? loopCtx.state.providerId;
|
||||
const sandboxInstance = getSandboxInstance(loopCtx, loopCtx.state.workspaceId, providerId, sandbox.sandboxId);
|
||||
|
||||
const cwd =
|
||||
sandbox.metadata && typeof (sandbox.metadata as any).cwd === "string"
|
||||
? ((sandbox.metadata as any).cwd as string)
|
||||
: undefined;
|
||||
|
||||
return await sandboxInstance.createSession({
|
||||
prompt: buildAgentPrompt(loopCtx.state.task),
|
||||
cwd,
|
||||
agent: (loopCtx.state.agentType ?? config.default_agent) as any
|
||||
});
|
||||
return {
|
||||
id: null,
|
||||
status: "idle",
|
||||
deferred: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export async function initWriteDbActivity(
|
||||
|
|
@ -432,10 +421,13 @@ export async function initWriteDbActivity(
|
|||
const now = Date.now();
|
||||
const db = loopCtx.db;
|
||||
const sessionId = session?.id ?? null;
|
||||
const sessionHealthy = Boolean(sessionId) && session?.status !== "error";
|
||||
const activeSessionId = sessionHealthy ? sessionId : null;
|
||||
const sessionDeferred = !sessionId;
|
||||
const sessionHealthy = sessionDeferred || session?.status !== "error";
|
||||
const activeSessionId = sessionDeferred ? null : sessionHealthy ? sessionId : null;
|
||||
const statusMessage =
|
||||
sessionHealthy
|
||||
sessionDeferred
|
||||
? "ready"
|
||||
: sessionHealthy
|
||||
? "session created"
|
||||
: session?.status === "error"
|
||||
? (session.error ?? "session create failed")
|
||||
|
|
@ -454,7 +446,7 @@ export async function initWriteDbActivity(
|
|||
.update(handoffTable)
|
||||
.set({
|
||||
providerId,
|
||||
status: sessionHealthy ? "running" : "error",
|
||||
status: sessionHealthy ? "idle" : "error",
|
||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||
updatedAt: now
|
||||
})
|
||||
|
|
@ -549,7 +541,7 @@ export async function initStartStatusSyncActivity(
|
|||
export async function initCompleteActivity(loopCtx: any, body: any, sandbox: any, session: any): Promise<void> {
|
||||
const providerId = body?.providerId ?? loopCtx.state.providerId;
|
||||
const sessionId = session?.id ?? null;
|
||||
const sessionHealthy = Boolean(sessionId) && session?.status !== "error";
|
||||
const sessionHealthy = !sessionId || session?.status !== "error";
|
||||
if (sessionHealthy) {
|
||||
await setHandoffState(loopCtx, "init_complete", "handoff initialized");
|
||||
|
||||
|
|
@ -558,7 +550,7 @@ export async function initCompleteActivity(loopCtx: any, body: any, sandbox: any
|
|||
kind: "handoff.initialized",
|
||||
handoffId: loopCtx.state.handoffId,
|
||||
branchName: loopCtx.state.branchName,
|
||||
payload: { providerId, sandboxId: sandbox.sandboxId, sessionId }
|
||||
payload: { providerId, sandboxId: sandbox.sandboxId, sessionId: sessionId ?? null }
|
||||
});
|
||||
|
||||
loopCtx.state.initialized = true;
|
||||
|
|
|
|||
79
factory/packages/backend/src/config/env.ts
Normal file
79
factory/packages/backend/src/config/env.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const DEVELOPMENT_ENV_FILES = [".env.development.local", ".env.development"] as const;
|
||||
const LOCAL_DEV_BETTER_AUTH_SECRET = "sandbox-agent-factory-development-only-change-me";
|
||||
const LOCAL_DEV_APP_URL = "http://localhost:4173";
|
||||
|
||||
function loadEnvFile(path: string): void {
|
||||
const source = readFileSync(path, "utf8");
|
||||
for (const line of source.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalized = trimmed.startsWith("export ") ? trimmed.slice("export ".length).trim() : trimmed;
|
||||
const separatorIndex = normalized.indexOf("=");
|
||||
if (separatorIndex <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = normalized.slice(0, separatorIndex).trim();
|
||||
if (!key || process.env[key] != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = normalized.slice(separatorIndex + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function isDevelopmentEnv(): boolean {
|
||||
return process.env.NODE_ENV === "development";
|
||||
}
|
||||
|
||||
export function loadDevelopmentEnvFiles(cwd = process.cwd()): string[] {
|
||||
if (!isDevelopmentEnv()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const loaded: string[] = [];
|
||||
for (const fileName of DEVELOPMENT_ENV_FILES) {
|
||||
const path = resolve(cwd, fileName);
|
||||
if (!existsSync(path)) {
|
||||
continue;
|
||||
}
|
||||
loadEnvFile(path);
|
||||
loaded.push(path);
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
export function applyDevelopmentEnvDefaults(): void {
|
||||
if (!isDevelopmentEnv()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.APP_URL) {
|
||||
process.env.APP_URL = LOCAL_DEV_APP_URL;
|
||||
}
|
||||
|
||||
if (!process.env.BETTER_AUTH_URL) {
|
||||
process.env.BETTER_AUTH_URL = process.env.APP_URL;
|
||||
}
|
||||
|
||||
if (!process.env.BETTER_AUTH_SECRET) {
|
||||
process.env.BETTER_AUTH_SECRET = LOCAL_DEV_BETTER_AUTH_SECRET;
|
||||
}
|
||||
|
||||
if (!process.env.GITHUB_REDIRECT_URI && process.env.APP_URL) {
|
||||
process.env.GITHUB_REDIRECT_URI = `${process.env.APP_URL.replace(/\/$/, "")}/api/rivet/app/auth/github/callback`;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { initActorRuntimeContext } from "./actors/context.js";
|
|||
import { registry, resolveManagerPort } from "./actors/index.js";
|
||||
import { workspaceKey } from "./actors/keys.js";
|
||||
import { loadConfig } from "./config/backend.js";
|
||||
import { applyDevelopmentEnvDefaults, loadDevelopmentEnvFiles } from "./config/env.js";
|
||||
import { createBackends, createNotificationService } from "./notifications/index.js";
|
||||
import { createDefaultDriver } from "./driver.js";
|
||||
import { createProviderRegistry } from "./providers/index.js";
|
||||
|
|
@ -17,6 +18,9 @@ export interface BackendStartOptions {
|
|||
}
|
||||
|
||||
export async function startBackend(options: BackendStartOptions = {}): Promise<void> {
|
||||
loadDevelopmentEnvFiles();
|
||||
applyDevelopmentEnvDefaults();
|
||||
|
||||
// sandbox-agent agent plugins vary on which env var they read for OpenAI/Codex auth.
|
||||
// Normalize to keep local dev + docker-compose simple.
|
||||
if (!process.env.CODEX_API_KEY && process.env.OPENAI_API_KEY) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type {
|
|||
} from "sandbox-agent";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
export type AgentId = AgentType | "opencode";
|
||||
export type AgentId = AgentType | "opencode" | "mock";
|
||||
|
||||
export interface SandboxSession {
|
||||
id: string;
|
||||
|
|
@ -37,6 +37,24 @@ export interface SandboxAgentClientOptions {
|
|||
|
||||
const DEFAULT_AGENT: AgentId = "codex";
|
||||
|
||||
function hasClaudeCredentials(): boolean {
|
||||
return Boolean(process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY);
|
||||
}
|
||||
|
||||
function hasCodexCredentials(): boolean {
|
||||
return Boolean(process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY);
|
||||
}
|
||||
|
||||
function resolveAvailableAgent(requested: AgentId): AgentId {
|
||||
if (requested === "claude") {
|
||||
return hasClaudeCredentials() ? requested : "mock";
|
||||
}
|
||||
if (requested === "codex") {
|
||||
return hasCodexCredentials() ? requested : "mock";
|
||||
}
|
||||
return requested;
|
||||
}
|
||||
|
||||
function modeIdForAgent(agent: AgentId): string | null {
|
||||
switch (agent) {
|
||||
case "codex":
|
||||
|
|
@ -130,18 +148,19 @@ export class SandboxAgentClient {
|
|||
typeof request === "string"
|
||||
? { prompt: request }
|
||||
: request;
|
||||
const resolvedAgent = resolveAvailableAgent(normalized.agent ?? this.agent);
|
||||
const sdk = await this.sdk();
|
||||
// Do not wrap createSession in a local Promise.race timeout. The underlying SDK
|
||||
// call is not abortable, so local timeout races create overlapping ACP requests and
|
||||
// can produce duplicate/orphaned sessions while the original request is still running.
|
||||
const session = await sdk.createSession({
|
||||
agent: normalized.agent ?? this.agent,
|
||||
agent: resolvedAgent,
|
||||
sessionInit: {
|
||||
cwd: normalized.cwd ?? "/",
|
||||
mcpServers: [],
|
||||
},
|
||||
});
|
||||
const modeId = modeIdForAgent(normalized.agent ?? this.agent);
|
||||
const modeId = modeIdForAgent(resolvedAgent);
|
||||
|
||||
// Codex defaults to a restrictive "read-only" preset in some environments.
|
||||
// For Sandbox Agent Factory automation we need to allow edits + command execution + network
|
||||
|
|
@ -359,7 +378,7 @@ export class SandboxAgentClient {
|
|||
|
||||
const sdk = await this.sdk();
|
||||
const session = await sdk.createSession({
|
||||
agent: this.agent,
|
||||
agent: resolveAvailableAgent(this.agent),
|
||||
sessionInit: {
|
||||
cwd: dir,
|
||||
mcpServers: [],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue