这个模块负责把一次“让后台 Agent 去改代码”的请求,落到一个可以实际工作的开发环境里。它不只创建一条会话记录,还要准备仓库、终端、端口、IDE、认证和实时连接。
在 Open-Inspect 里,一次典型任务会经历下面这些步骤:
这个模块的边界很清楚:它负责“运行环境”,不决定 Agent 如何推理,也不决定 PR 文案怎么写。
一次会话不是单纯的数据库行,而是控制平面和沙箱之间的一组状态流转。
常见状态可以理解为:
textcreated ↓ provisioning ↓ starting ↓ ready ↓ running ↓ completed / failed / stopped ↓ archived / cleaned
各状态的职责:
| 状态 | 发生了什么 |
|---|---|
created |
用户提交任务,控制平面记录 prompt、仓库、触发来源、用户身份 |
provisioning |
开始分配沙箱资源,例如容器、VM 或远程执行环境 |
starting |
沙箱启动基础服务,包括 agent runtime、终端服务、端口代理等 |
ready |
仓库已克隆,工作目录可用,终端和实时通道可以连接 |
running |
Agent 正在执行任务,用户可以实时观察和介入 |
completed |
Agent 完成任务,可能已经提交 commit、创建 PR |
failed |
沙箱启动失败、克隆失败、命令失败或 Agent 异常退出 |
stopped |
用户手动停止 |
archived |
会话只保留日志、diff、PR 链接等结果信息 |
cleaned |
沙箱运行资源被释放 |
会话里通常会保存这些信息:
tstype Session = {
id: string;
userId: string;
prompt: string;
status:
| "created"
| "provisioning"
| "starting"
| "ready"
| "running"
| "completed"
| "failed"
| "stopped";
repositories: RepositorySpec[];
primaryRepository: string;
sandboxId?: string;
workspacePath?: string;
createdAt: string;
updatedAt: string;
completedAt?: string;
};实际项目里的字段名可能不同,但核心语义就是:会话负责把“用户任务”和“沙箱运行实例”绑定起来。
沙箱是 Agent 真正工作的地方。它需要像一个完整开发机,而不是一个只能跑脚本的临时容器。
沙箱至少需要具备:
沙箱启动时,控制平面会给它一个只属于当前会话的 token。这个 token 不等于 GitHub token,它只用于沙箱向控制平面证明自己属于某个会话。
简化后的启动参数可以理解为:
bashSESSION_ID="sess_123"
SANDBOX_AUTH_TOKEN="sandbox_session_scoped_token"
CONTROL_PLANE_URL="https://inspect.example.com"
WORKSPACE_DIR="/workspace"沙箱用这些参数完成注册:
httpPOST /api/sessions/sess_123/sandbox/heartbeat
Authorization: Bearer sandbox_session_scoped_token
Content-Type: application/json
{
"status": "ready",
"workspace": "/workspace",
"capabilities": {
"terminal": true,
"ports": true,
"vscode": true
}
}重点是:沙箱 token 是会话级别的,不应该跨会话复用。
Open-Inspect 的 README 明确说明了它的安全模型:系统使用共享 GitHub App installation token 处理 git clone、fetch、push。
流程是:
text沙箱执行 git clone ↓ git credential helper 被触发 ↓ credential helper 向控制平面请求 GitHub App token ↓ 控制平面 mint 短期 installation token ↓ 沙箱拿到 token,用于本次 git 操作
这样做的好处是沙箱里不需要长期保存 GitHub App 私钥,也不需要把组织级凭据写进镜像。
一个典型的 credential helper 行为可以抽象成:
bashgit config --global credential.helper \
"!/usr/local/bin/open-inspect-git-credential-helper"当 git 需要凭据时,会向 helper 传入:
textprotocol=https host=github.com path=org/repo.git
helper 再向控制平面请求短期凭据:
httpPOST /api/git/credentials
Authorization: Bearer sandbox_session_scoped_token
Content-Type: application/json
{
"host": "github.com",
"path": "org/repo.git",
"operation": "clone"
}控制平面返回:
json{
"username": "x-access-token",
"password": "ghs_short_lived_installation_token",
"expiresAt": "2026-07-12T12:30:00.000Z"
}沙箱拿到后只把它交给 git 当前操作使用,不应该落盘保存。
最常见的会话只绑定一个仓库。
工作区布局可以是:
text/workspace └── repo ├── package.json ├── src └── tests
会话创建时通常会带上:
json{
"prompt": "修复登录页在 Safari 下按钮错位的问题",
"repositories": [
{
"owner": "acme",
"repo": "web",
"defaultBranch": "main",
"checkoutBranch": "main"
}
],
"primaryRepository": "acme/web"
}沙箱侧执行:
bashmkdir -p /workspace
cd /workspace
git clone https://github.com/acme/web.git repo
cd repo
git checkout main
git checkout -b inspect/sess_123-fix-safari-login-buttonAgent 后续所有命令默认在 /workspace/repo 下执行。
Open-Inspect 支持多仓库环境。这个能力很重要,因为真实任务经常跨仓库:
多仓库工作区通常采用平铺目录:
text/workspace ├── web ├── api └── sdk
会话请求可以理解为:
json{
"prompt": "把新的 billing status 字段从 API 接到前端页面",
"repositories": [
{
"owner": "acme",
"repo": "api",
"path": "api"
},
{
"owner": "acme",
"repo": "web",
"path": "web"
},
{
"owner": "acme",
"repo": "sdk",
"path": "sdk"
}
],
"primaryRepository": "acme/web"
}沙箱准备工作区:
bashmkdir -p /workspace
cd /workspace
git clone https://github.com/acme/api.git api
git clone https://github.com/acme/web.git web
git clone https://github.com/acme/sdk.git sdk推荐把“主仓库”单独标出来,因为最终 PR 往往只创建在一个仓库上。如果 Agent 修改了多个仓库,需要明确是:
Agent 常常需要启动开发服务器,例如:
bashnpm run dev
服务可能监听在沙箱内部的 localhost:3000。用户浏览器无法直接访问沙箱内部地址,所以需要端口隧道。
端口隧道的职责是把:
textsandbox:localhost:3000
映射成:
texthttps://sess-123-3000.inspect.example.com
或者:
texthttps://inspect.example.com/sessions/sess_123/ports/3000
一个端口记录可以抽象成:
tstype PortForward = {
sessionId: string;
port: number;
protocol: "http" | "https" | "tcp";
targetHost: "127.0.0.1";
publicUrl: string;
status: "opening" | "open" | "closed";
};创建端口转发:
httpPOST /api/sessions/sess_123/ports
Authorization: Bearer websocket_or_user_token
Content-Type: application/json
{
"port": 3000,
"protocol": "http"
}返回:
json{
"port": 3000,
"publicUrl": "https://sess-123-3000.inspect.example.com",
"status": "open"
}这个能力不仅给用户看页面,也给 Agent 做浏览器自动化用。例如 Agent 可以启动 Web 应用,然后用 Playwright 访问端口,验证 UI 是否修好。
终端能力用于两件事:
终端通常需要伪终端 PTY,而不是简单的 exec,因为很多开发命令依赖 TTY 行为:
一个终端连接可以理解为:
textWeb UI ↓ WebSocket 控制平面 ↓ session-scoped channel 沙箱 PTY
WebSocket 消息一般会包含几类:
json{ "type": "input", "data": "npm test\n" }json{ "type": "output", "data": "PASS src/login.test.ts\n" }json{ "type": "resize", "cols": 120, "rows": 40 }json{ "type": "exit", "code": 0 }终端和 Agent 命令执行可以共用同一个沙箱,但不要混用同一个 shell 会话。推荐:
VS Code 能力让用户直接进入沙箱工作区,查看 Agent 改动、运行测试、手动修复。
常见实现方式是:
工作流:
text用户点击 “Open in VS Code” ↓ 控制平面校验用户是否可访问该 session ↓ 生成一次性或短期 IDE token ↓ 打开 https://sess-123-vscode.inspect.example.com ↓ VS Code 进入 /workspace/repo
IDE 入口可以抽象成:
httpPOST /api/sessions/sess_123/ide
Authorization: Bearer websocket_or_user_token
Content-Type: application/json
{
"workspace": "/workspace/repo"
}返回:
json{
"url": "https://sess-123-vscode.inspect.example.com",
"expiresAt": "2026-07-12T13:00:00.000Z"
}VS Code 入口不要直接暴露沙箱裸端口,应该经过控制平面的鉴权或隧道鉴权。
会话创建接口负责接收任务和仓库信息。
httpPOST /api/sessions
Content-Type: application/json
Authorization: Bearer user_token
{
"prompt": "修复 checkout 页面 coupon 失效的问题",
"repositories": [
{
"owner": "acme",
"repo": "web",
"branch": "main",
"path": "web"
}
],
"primaryRepository": "acme/web"
}返回值包含会话 ID:
json{
"id": "sess_123",
"status": "created"
}创建后,后台 worker 会把会话推进到 provisioning。
沙箱分配接口通常只在服务端内部调用。
tstype SandboxCreateInput = {
sessionId: string;
image: string;
env: Record<string, string>;
workspaceDir: string;
};
type SandboxCreateResult = {
sandboxId: string;
status: "starting";
};示例:
tsawait sandboxProvider.create({
sessionId: "sess_123",
image: "open-inspect/sandbox:latest",
env: {
SESSION_ID: "sess_123",
CONTROL_PLANE_URL: "https://inspect.example.com",
SANDBOX_AUTH_TOKEN: sandboxToken,
WORKSPACE_DIR: "/workspace"
},
workspaceDir: "/workspace"
});这里的 sandboxProvider 可以对应不同后端:本地 Docker、Kubernetes、Firecracker、云厂商 VM 等。模块上层不应该绑定某一种实现。
沙箱启动后需要持续告诉控制平面自己还活着。
httpPOST /api/sessions/sess_123/sandbox/heartbeat
Authorization: Bearer sandbox_session_scoped_token
Content-Type: application/json
{
"sandboxId": "sbx_456",
"status": "ready",
"timestamp": "2026-07-12T12:00:00.000Z"
}控制平面可以用心跳做:
仓库克隆最好由沙箱启动脚本统一完成,而不是让 Agent 第一步自己 clone。这样 Agent 进入时已经有稳定工作区。
bash#!/usr/bin/env bash
set -euo pipefail
mkdir -p "$WORKSPACE_DIR"
cd "$WORKSPACE_DIR"
for repo in "$@"; do
# repo 格式示例:acme/web:web
spec="${repo%%:*}"
path="${repo##*:}"
git clone "https://github.com/${spec}.git" "$path"
done调用:
bashclone-repos.sh acme/web:web acme/api:api
生成:
text/workspace ├── web └── api
端口隧道接口需要记录 session、端口和访问 URL。
tstype OpenPortInput = {
sessionId: string;
port: number;
protocol: "http" | "https";
};
type OpenPortResult = {
publicUrl: string;
port: number;
};示例:
tsconst port = await portTunnel.open({
sessionId: "sess_123",
port: 3000,
protocol: "http"
});
console.log(port.publicUrl);终端需要保持连接,所以一般用 WebSocket。
textGET /api/sessions/sess_123/terminal?token=ws_session_token
消息格式可以保持简单:
tstype TerminalMessage =
| { type: "input"; data: string }
| { type: "output"; data: string }
| { type: "resize"; cols: number; rows: number }
| { type: "exit"; code: number };IDE 入口应该由服务端生成,而不是直接暴露沙箱内部 URL。
tstype CreateIdeUrlInput = {
sessionId: string;
workspacePath: string;
};
type CreateIdeUrlResult = {
url: string;
expiresAt: string;
};示例:
tsconst ide = await ideService.createUrl({
sessionId: "sess_123",
workspacePath: "/workspace/web"
});
return ide.url;下面的示例不依赖 Open-Inspect 服务端,可以在本机直接运行。它模拟了一个沙箱会话会做的几件事:
保存为 demo-session-sandbox.sh:
bash#!/usr/bin/env bash
set -euo pipefail
SESSION_ID="sess_demo_$(date +%s)"
ROOT_DIR="$(mktemp -d)"
WORKSPACE_DIR="$ROOT_DIR/workspace"
echo "Session: $SESSION_ID"
echo "Workspace: $WORKSPACE_DIR"
mkdir -p "$WORKSPACE_DIR"
echo
echo "==> 创建模拟远程仓库"
REMOTE_DIR="$ROOT_DIR/remotes"
mkdir -p "$REMOTE_DIR"
create_remote_repo() {
local name="$1"
local path="$REMOTE_DIR/$name"
mkdir -p "$path"
cd "$path"
git init -q
git config user.email "demo@example.com"
git config user.name "Demo User"
cat > README.md <<EOF
# $name
This is a demo repository for $SESSION_ID.
EOF
git add README.md
git commit -q -m "Initial commit"
}
create_remote_repo "web"
create_remote_repo "api"
echo
echo "==> 克隆多仓库到沙箱工作区"
cd "$WORKSPACE_DIR"
git clone -q "$REMOTE_DIR/web" web
git clone -q "$REMOTE_DIR/api" api
echo
echo "==> 在主仓库创建 Agent 工作分支"
cd "$WORKSPACE_DIR/web"
git checkout -q -b "inspect/$SESSION_ID-demo-change"
cat > index.html <<'EOF'
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Open-Inspect Sandbox Demo</title>
</head>
<body>
<h1>Sandbox is running</h1>
<p>This page is served from the session workspace.</p>
</body>
</html>
EOF
git add index.html
git commit -q -m "Add demo page"
echo
echo "==> 启动开发服务器"
PORT="3000"
python3 -m http.server "$PORT" > "$ROOT_DIR/server.log" 2>&1 &
SERVER_PID="$!"
sleep 1
echo "Local dev server: http://127.0.0.1:$PORT"
echo "Tunnel-like URL: http://localhost:$PORT"
echo "Server PID: $SERVER_PID"
echo "Server log: $ROOT_DIR/server.log"
echo
echo "==> 当前工作区布局"
find "$WORKSPACE_DIR" -maxdepth 2 -type d | sort
echo
echo "==> 主仓库 git 状态"
cd "$WORKSPACE_DIR/web"
git --no-pager log --oneline --decorate -3
git status --short
echo
echo "==> 如果安装了 VS Code,可以打开工作区"
if command -v code >/dev/null 2>&1; then
echo "运行:code $WORKSPACE_DIR/web"
else
echo "未找到 code 命令,跳过"
fi
echo
echo "==> 访问页面"
echo "curl http://127.0.0.1:$PORT"
echo
echo "==> 清理命令"
echo "kill $SERVER_PID"
echo "rm -rf $ROOT_DIR"运行:
bashchmod +x demo-session-sandbox.sh
./demo-session-sandbox.sh验证开发服务器:
bashcurl http://127.0.0.1:3000
如果安装了 VS Code:
bashcode /tmp/上面输出的目录/workspace/web
这个脚本展示的是“沙箱运行能力”的最小闭环。真实 Open-Inspect 部署里,mktemp 会替换成远程沙箱,localhost:3000 会替换成端口隧道 URL,git 本地路径会替换成 GitHub App token 认证的 HTTPS clone。
下面的脚本演示 Agent 如何在多仓库环境中读取 API 仓库的信息,并修改 Web 仓库。
保存为 demo-multi-repo-task.sh:
bash#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(mktemp -d)"
WORKSPACE_DIR="$ROOT_DIR/workspace"
mkdir -p "$WORKSPACE_DIR/api" "$WORKSPACE_DIR/web"
cd "$WORKSPACE_DIR/api"
git init -q
git config user.email "demo@example.com"
git config user.name "Demo User"
cat > billing-status.json <<'EOF'
{
"statuses": ["active", "past_due", "canceled"]
}
EOF
git add billing-status.json
git commit -q -m "Add billing status contract"
cd "$WORKSPACE_DIR/web"
git init -q
git config user.email "demo@example.com"
git config user.name "Demo User"
cat > render.js <<'EOF'
function renderBillingStatus(status) {
return `Billing status: ${status}`;
}
module.exports = { renderBillingStatus };
EOF
git add render.js
git commit -q -m "Add billing renderer"
echo "==> Agent 读取 api 仓库 contract,并修改 web 仓库"
STATUSES="$(node -e "const s=require('$WORKSPACE_DIR/api/billing-status.json').statuses; console.log(s.join(', '))")"
cat > "$WORKSPACE_DIR/web/render.js" <<EOF
const knownStatuses = "$STATUSES".split(", ");
function renderBillingStatus(status) {
if (!knownStatuses.includes(status)) {
return "Billing status: unknown";
}
return \`Billing status: \${status}\`;
}
module.exports = { renderBillingStatus, knownStatuses };
EOF
cd "$WORKSPACE_DIR/web"
git diff -- render.js
echo
echo "Workspace: $WORKSPACE_DIR"
echo "Cleanup: rm -rf $ROOT_DIR"运行:
bashchmod +x demo-multi-repo-task.sh
./demo-multi-repo-task.sh这个例子对应真实场景:api 仓库提供契约,web 仓库消费契约。会话配置里应把 web 标为主仓库,避免 Agent 不小心在多个仓库里同时提交不可控变更。
README 已经明确说明:Open-Inspect 的安全模型是 single-tenant only。
原因是:
适合的部署方式:
不适合:
Open-Inspect 的 token 架构里有两个关键凭据:
| 凭据 | 用途 |
|---|---|
| GitHub App Token | clone、fetch、push 等 git 操作 |
| User OAuth Token | 创建 PR、用户信息、PR 归属 |
这意味着:
所以不要把“能 clone”理解成“用户本人有权限”。
多仓库环境很方便,但也容易扩大变更范围。建议在会话配置里明确:
json{
"primaryRepository": "acme/web",
"repositories": [
{
"owner": "acme",
"repo": "web",
"path": "web",
"writable": true
},
{
"owner": "acme",
"repo": "api",
"path": "api",
"writable": false
}
]
}如果项目暂时没有 writable 字段,也应该在 prompt 和系统约束里写清楚:
textapi 仓库只用于读取接口定义,不要修改或提交 api 仓库。 只在 web 仓库创建 commit 和 PR。
端口隧道暴露的是沙箱里运行的服务。开发服务器经常没有登录保护,甚至可能暴露调试页面、环境变量、内部接口。
因此端口隧道应该至少满足:
终端等价于给用户一个沙箱 shell。即使沙箱是隔离环境,也要注意:
如果要支持多人协作,建议在日志里记录:
VS Code 或 code-server 一旦暴露,就可以访问整个工作区文件。不要直接开放类似:
texthttp://sandbox-internal-ip:8080
更安全的做法是:
text用户 → 控制平面鉴权 → 短期 IDE URL → 端口隧道 → 沙箱 IDE 服务
并且:
实际运行里,下面情况都很常见:
模块实现时需要把失败作为正常路径处理:
沙箱可以降低风险,但不能替代组织级访问控制。尤其在这个项目的单租户模型下,应同时做好:
最重要的一条:GitHub App 不要安装到不希望 Agent 访问的仓库。