feat(agent): ensure clean container and isolate workspace artifacts strictly to host volume

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-13 02:50:18 +00:00
committed by weichen
co-authored by Cursor
parent 3ffcab4502
commit 4c35888a4e
6 changed files with 94 additions and 16 deletions
+27 -3
View File
@@ -13,9 +13,13 @@ import (
func main() { func main() {
taskKey := flag.String("task", "intake", "任务标识 (intake, research, cdo_quote 等)") taskKey := flag.String("task", "intake", "任务标识 (intake, research, cdo_quote 等)")
backend := flag.String("backend", "stub", "智能体后端 (stub, pi-agent, cursor-cli)") backend := flag.String("backend", "", "智能体后端 (留空则读取 .env 配置,可选: stub, pi-agent, cursor-cli)")
project := flag.String("project", "医疗器械研发演示项目", "项目名称") project := flag.String("project", "医疗器械研发演示项目", "项目名称")
customer := flag.String("customer", "北京水木东方", "客户名称") customer := flag.String("customer", "北京水木东方", "客户名称")
device := flag.String("device", "医疗器械", "设备类型")
use := flag.String("use", "临床诊疗", "预期用途")
market := flag.String("market", "国内", "目标市场")
materials := flag.String("materials", "", "输入补充材料内容")
envPath := flag.String("env", ".env", "环境配置文件路径") envPath := flag.String("env", ".env", "环境配置文件路径")
flag.Parse() flag.Parse()
@@ -25,16 +29,32 @@ func main() {
os.Exit(1) os.Exit(1)
} }
activeBackend := cfg.AgentBackend
if *backend != "" { if *backend != "" {
activeBackend = *backend
cfg.AgentBackend = *backend cfg.AgentBackend = *backend
} }
var materialsList []bizdev.MaterialInput
if *materials != "" {
materialsList = []bizdev.MaterialInput{
{
Filename: "user_input.md",
Content: []byte(*materials),
},
}
}
svc := bizdev.NewService(cfg) svc := bizdev.NewService(cfg)
res, err := svc.ExecuteTask(context.Background(), bizdev.TaskRunRequest{ res, err := svc.ExecuteTask(context.Background(), bizdev.TaskRunRequest{
TaskKey: *taskKey, TaskKey: *taskKey,
ProjectName: *project, ProjectName: *project,
CustomerName: *customer, CustomerName: *customer,
Backend: *backend, DeviceType: *device,
IntendedUse: *use,
TargetMarket: *market,
Materials: materialsList,
Backend: activeBackend,
}) })
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "任务执行失败: %v\n", err) fmt.Fprintf(os.Stderr, "任务执行失败: %v\n", err)
@@ -47,7 +67,11 @@ func main() {
fmt.Printf("=== 任务执行成功 [%s] ===\n", res.TaskKey) fmt.Printf("=== 任务执行成功 [%s] ===\n", res.TaskKey)
fmt.Printf("耗时: %v\n", res.Duration) fmt.Printf("耗时: %v\n", res.Duration)
fmt.Printf("工作区: %s\n", res.WorkDir) fmt.Printf("宿主机受控工作区: %s\n", res.WorkDir)
fmt.Printf("交付文档产物: %s\n", filepath.Join(res.WorkDir, "output.md"))
if _, err := os.Stat(filepath.Join(res.WorkDir, "execution.log")); err == nil {
fmt.Printf("执行审计日志: %s\n", filepath.Join(res.WorkDir, "execution.log"))
}
fmt.Printf("使用的后端: %s\n", res.BackendUsed) fmt.Printf("使用的后端: %s\n", res.BackendUsed)
if len(res.Attachments) > 0 { if len(res.Attachments) > 0 {
fmt.Printf("附加产物:\n") fmt.Printf("附加产物:\n")
+5 -2
View File
@@ -76,11 +76,14 @@ cat <<EOF > "${PI_DIR}/models.json"
} }
EOF EOF
# 5. 生成 settings.json 设置默认 Provider 和 Model # 5. 生成 settings.json 设置默认 Provider、Model 与全局扩展
cat <<EOF > "${PI_DIR}/settings.json" cat <<EOF > "${PI_DIR}/settings.json"
{ {
"defaultProvider": "minimax", "defaultProvider": "minimax",
"defaultModel": "${MODEL_ID}" "defaultModel": "${MODEL_ID}",
"extensions": [
"/app/extensions/tavily-search.mjs"
]
} }
EOF EOF
+53 -11
View File
@@ -28,21 +28,25 @@ function safeRel(workspace, target) {
function resolveWorkspace(rawPath) { function resolveWorkspace(rawPath) {
if (!rawPath) return DATA_DIR; if (!rawPath) return DATA_DIR;
if (existsSync(rawPath)) return rawPath;
// If path contains 'workspaces/', extract the relative part // If path contains 'workspaces/', extract the relative part
const wsIndex = rawPath.indexOf("workspaces/"); const wsIndex = rawPath.indexOf("workspaces/");
let sub = "";
if (wsIndex !== -1) { if (wsIndex !== -1) {
const sub = rawPath.slice(wsIndex + "workspaces/".length); sub = rawPath.slice(wsIndex + "workspaces/".length);
return resolve(DATA_DIR, sub); } else if (rawPath.startsWith(DATA_DIR)) {
sub = relative(DATA_DIR, rawPath);
} else {
sub = rawPath.replace(/^[/\\]+/, "");
} }
// If relative path // Force all execution strictly inside DATA_DIR (which is mounted to host's controlled directory)
if (!isAbsolute(rawPath)) { const target = resolve(DATA_DIR, sub);
return resolve(DATA_DIR, rawPath); const rel = relative(DATA_DIR, target);
if (rel.startsWith("..") || isAbsolute(rel)) {
return resolve(DATA_DIR, "sandboxes", "isolated_" + Date.now());
} }
return target;
return rawPath;
} }
function parseJsonBody(req) { function parseJsonBody(req) {
@@ -94,7 +98,7 @@ async function handleRun(body) {
if (rel) allowed.add(rel); if (rel) allowed.add(rel);
} }
// 1. Write persona instructions into workspace // 1. Write persona instructions and task into workspace
if (instructions.trim()) { if (instructions.trim()) {
try { try {
writeFileSync(resolve(workspace, "AGENTS.md"), instructions, "utf8"); writeFileSync(resolve(workspace, "AGENTS.md"), instructions, "utf8");
@@ -104,10 +108,19 @@ async function handleRun(body) {
} }
} }
if (prompt.trim()) {
try {
writeFileSync(resolve(workspace, "TASK.md"), prompt, "utf8");
} catch (e) {
console.warn("[runner] Failed to write TASK.md:", e);
}
}
// 2. Build pi arguments // 2. Build pi arguments
const piBin = process.env.PI_AGENT_BIN || "pi"; const piBin = process.env.PI_AGENT_BIN || "pi";
const args = [ const args = [
"--mode", "rpc", "--mode", "rpc",
"-e", "/app/extensions/tavily-search.mjs",
"--no-session", "--no-session",
"--provider", provider, "--provider", provider,
"--model", model, "--model", model,
@@ -156,8 +169,11 @@ async function handleRun(body) {
); );
let stdoutBuf = ""; let stdoutBuf = "";
let fullStdout = "";
child.stdout.on("data", (chunk) => { child.stdout.on("data", (chunk) => {
stdoutBuf += chunk.toString("utf8"); const text = chunk.toString("utf8");
stdoutBuf += text;
fullStdout += text;
let nl; let nl;
while ((nl = stdoutBuf.indexOf("\n")) >= 0) { while ((nl = stdoutBuf.indexOf("\n")) >= 0) {
const line = stdoutBuf.slice(0, nl).trim(); const line = stdoutBuf.slice(0, nl).trim();
@@ -197,6 +213,23 @@ async function handleRun(body) {
}); });
}); });
// Ensure all intermediate traces, logs and thinking process land on the host-mounted workspace
try {
const trace = [
`=== PI-AGENT TASK EXECUTION LOG ===`,
`Timestamp: ${new Date().toISOString()}`,
`Workspace: ${workspace}`,
`Model: ${model} (${provider})`,
`Outcome: ${succeeded ? "SUCCESS" : "FAILED"}`,
`Error: ${lastErr || "None"}`,
`\n--- RAW OUTPUT & EVENTS ---\n${fullStdout}`,
`\n--- STDERR ---\n${stderrBuf}`,
].join("\n");
writeFileSync(resolve(workspace, "execution.log"), trace, "utf8");
} catch (logErr) {
console.warn("[runner] Could not write execution.log:", logErr.message);
}
if (!succeeded) { if (!succeeded) {
return { return {
ok: false, ok: false,
@@ -208,7 +241,7 @@ async function handleRun(body) {
}; };
} }
// 3. Read generated files // 3. Read generated files and deliverables
const patches = []; const patches = [];
for (const rel of allowed) { for (const rel of allowed) {
const abs = resolve(workspace, rel); const abs = resolve(workspace, rel);
@@ -217,6 +250,15 @@ async function handleRun(body) {
patches.push({ rel_path: rel, content }); patches.push({ rel_path: rel, content });
} }
// Also include execution.log as patch if present so host definitely receives it
const execLogPath = resolve(workspace, "execution.log");
if (existsSync(execLogPath)) {
try {
const logContent = readFileSync(execLogPath, "utf8");
patches.push({ rel_path: "execution.log", content: logContent });
} catch {}
}
return { return {
ok: true, ok: true,
agent_id: agentId, agent_id: agentId,
+3
View File
@@ -65,6 +65,9 @@ services:
- "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18865}:18865" - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18865}:18865"
volumes: volumes:
- ./data/dev/workspaces:/app/data/workspaces - ./data/dev/workspaces:/app/data/workspaces
- ./data/dev/agent_sessions:/root/.pi/agent/sessions
tmpfs:
- /tmp
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl -f http://127.0.0.1:18865/health || exit 1"] test: ["CMD-SHELL", "curl -f http://127.0.0.1:18865/health || exit 1"]
interval: 10s interval: 10s
+3
View File
@@ -65,6 +65,9 @@ services:
- "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18867}:18867" - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18867}:18867"
volumes: volumes:
- ./data/main/workspaces:/app/data/workspaces - ./data/main/workspaces:/app/data/workspaces
- ./data/main/agent_sessions:/root/.pi/agent/sessions
tmpfs:
- /tmp
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl -f http://127.0.0.1:18867/health || exit 1"] test: ["CMD-SHELL", "curl -f http://127.0.0.1:18867/health || exit 1"]
interval: 10s interval: 10s
+3
View File
@@ -65,6 +65,9 @@ services:
- "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18866}:18866" - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18866}:18866"
volumes: volumes:
- ./data/test/workspaces:/app/data/workspaces - ./data/test/workspaces:/app/data/workspaces
- ./data/test/agent_sessions:/root/.pi/agent/sessions
tmpfs:
- /tmp
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl -f http://127.0.0.1:18866/health || exit 1"] test: ["CMD-SHELL", "curl -f http://127.0.0.1:18866/health || exit 1"]
interval: 10s interval: 10s