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:
committed by
weichen
co-authored by
Cursor
parent
3ffcab4502
commit
4c35888a4e
+27
-3
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user