From 4c35888a4e399f0e91f5348a9ec45b62831dbe58 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Mon, 7 Sep 2026 17:51:50 +0800 Subject: [PATCH] feat(agent): ensure clean container and isolate workspace artifacts strictly to host volume Co-authored-by: Cursor --- cmd/bizdev-cli/main.go | 30 ++++++++++++++-- deploy/pi-agent/entrypoint.sh | 7 ++-- deploy/pi-agent/runner.mjs | 64 +++++++++++++++++++++++++++++------ docker-compose.dev.yml | 3 ++ docker-compose.main.yml | 3 ++ docker-compose.test.yml | 3 ++ 6 files changed, 94 insertions(+), 16 deletions(-) diff --git a/cmd/bizdev-cli/main.go b/cmd/bizdev-cli/main.go index fc6d4624..18fde0f6 100644 --- a/cmd/bizdev-cli/main.go +++ b/cmd/bizdev-cli/main.go @@ -13,9 +13,13 @@ import ( func main() { 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", "医疗器械研发演示项目", "项目名称") 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", "环境配置文件路径") flag.Parse() @@ -25,16 +29,32 @@ func main() { os.Exit(1) } + activeBackend := cfg.AgentBackend if *backend != "" { + activeBackend = *backend cfg.AgentBackend = *backend } + var materialsList []bizdev.MaterialInput + if *materials != "" { + materialsList = []bizdev.MaterialInput{ + { + Filename: "user_input.md", + Content: []byte(*materials), + }, + } + } + svc := bizdev.NewService(cfg) res, err := svc.ExecuteTask(context.Background(), bizdev.TaskRunRequest{ TaskKey: *taskKey, ProjectName: *project, CustomerName: *customer, - Backend: *backend, + DeviceType: *device, + IntendedUse: *use, + TargetMarket: *market, + Materials: materialsList, + Backend: activeBackend, }) if err != nil { fmt.Fprintf(os.Stderr, "任务执行失败: %v\n", err) @@ -47,7 +67,11 @@ func main() { fmt.Printf("=== 任务执行成功 [%s] ===\n", res.TaskKey) 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) if len(res.Attachments) > 0 { fmt.Printf("附加产物:\n") diff --git a/deploy/pi-agent/entrypoint.sh b/deploy/pi-agent/entrypoint.sh index e007edd0..d91b6fe6 100755 --- a/deploy/pi-agent/entrypoint.sh +++ b/deploy/pi-agent/entrypoint.sh @@ -76,11 +76,14 @@ cat < "${PI_DIR}/models.json" } EOF -# 5. 生成 settings.json 设置默认 Provider 和 Model +# 5. 生成 settings.json 设置默认 Provider、Model 与全局扩展 cat < "${PI_DIR}/settings.json" { "defaultProvider": "minimax", - "defaultModel": "${MODEL_ID}" + "defaultModel": "${MODEL_ID}", + "extensions": [ + "/app/extensions/tavily-search.mjs" + ] } EOF diff --git a/deploy/pi-agent/runner.mjs b/deploy/pi-agent/runner.mjs index b69201b9..08265feb 100755 --- a/deploy/pi-agent/runner.mjs +++ b/deploy/pi-agent/runner.mjs @@ -28,21 +28,25 @@ function safeRel(workspace, target) { function resolveWorkspace(rawPath) { if (!rawPath) return DATA_DIR; - if (existsSync(rawPath)) return rawPath; // If path contains 'workspaces/', extract the relative part const wsIndex = rawPath.indexOf("workspaces/"); + let sub = ""; if (wsIndex !== -1) { - const sub = rawPath.slice(wsIndex + "workspaces/".length); - return resolve(DATA_DIR, sub); + sub = rawPath.slice(wsIndex + "workspaces/".length); + } else if (rawPath.startsWith(DATA_DIR)) { + sub = relative(DATA_DIR, rawPath); + } else { + sub = rawPath.replace(/^[/\\]+/, ""); } - // If relative path - if (!isAbsolute(rawPath)) { - return resolve(DATA_DIR, rawPath); + // Force all execution strictly inside DATA_DIR (which is mounted to host's controlled directory) + const target = resolve(DATA_DIR, sub); + const rel = relative(DATA_DIR, target); + if (rel.startsWith("..") || isAbsolute(rel)) { + return resolve(DATA_DIR, "sandboxes", "isolated_" + Date.now()); } - - return rawPath; + return target; } function parseJsonBody(req) { @@ -94,7 +98,7 @@ async function handleRun(body) { if (rel) allowed.add(rel); } - // 1. Write persona instructions into workspace + // 1. Write persona instructions and task into workspace if (instructions.trim()) { try { 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 const piBin = process.env.PI_AGENT_BIN || "pi"; const args = [ "--mode", "rpc", + "-e", "/app/extensions/tavily-search.mjs", "--no-session", "--provider", provider, "--model", model, @@ -156,8 +169,11 @@ async function handleRun(body) { ); let stdoutBuf = ""; + let fullStdout = ""; child.stdout.on("data", (chunk) => { - stdoutBuf += chunk.toString("utf8"); + const text = chunk.toString("utf8"); + stdoutBuf += text; + fullStdout += text; let nl; while ((nl = stdoutBuf.indexOf("\n")) >= 0) { 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) { return { ok: false, @@ -208,7 +241,7 @@ async function handleRun(body) { }; } - // 3. Read generated files + // 3. Read generated files and deliverables const patches = []; for (const rel of allowed) { const abs = resolve(workspace, rel); @@ -217,6 +250,15 @@ async function handleRun(body) { 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 { ok: true, agent_id: agentId, diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index bb47cfcf..bc6ed967 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -65,6 +65,9 @@ services: - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18865}:18865" volumes: - ./data/dev/workspaces:/app/data/workspaces + - ./data/dev/agent_sessions:/root/.pi/agent/sessions + tmpfs: + - /tmp healthcheck: test: ["CMD-SHELL", "curl -f http://127.0.0.1:18865/health || exit 1"] interval: 10s diff --git a/docker-compose.main.yml b/docker-compose.main.yml index dcf26f45..872c212e 100644 --- a/docker-compose.main.yml +++ b/docker-compose.main.yml @@ -65,6 +65,9 @@ services: - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18867}:18867" volumes: - ./data/main/workspaces:/app/data/workspaces + - ./data/main/agent_sessions:/root/.pi/agent/sessions + tmpfs: + - /tmp healthcheck: test: ["CMD-SHELL", "curl -f http://127.0.0.1:18867/health || exit 1"] interval: 10s diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 682d2b23..6e392c9f 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -65,6 +65,9 @@ services: - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18866}:18866" volumes: - ./data/test/workspaces:/app/data/workspaces + - ./data/test/agent_sessions:/root/.pi/agent/sessions + tmpfs: + - /tmp healthcheck: test: ["CMD-SHELL", "curl -f http://127.0.0.1:18866/health || exit 1"] interval: 10s