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:
+27
-3
@@ -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")
|
||||
|
||||
@@ -76,11 +76,14 @@ cat <<EOF > "${PI_DIR}/models.json"
|
||||
}
|
||||
EOF
|
||||
|
||||
# 5. 生成 settings.json 设置默认 Provider 和 Model
|
||||
# 5. 生成 settings.json 设置默认 Provider、Model 与全局扩展
|
||||
cat <<EOF > "${PI_DIR}/settings.json"
|
||||
{
|
||||
"defaultProvider": "minimax",
|
||||
"defaultModel": "${MODEL_ID}"
|
||||
"defaultModel": "${MODEL_ID}",
|
||||
"extensions": [
|
||||
"/app/extensions/tavily-search.mjs"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
+53
-11
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user