Keep session state out of the process CWD for workspace-less requests
ci / vet-and-test (pull_request) Successful in 1m1s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s

Why: a ContractRequest without a workspace (the conformance minimal
case) resolved WorkDir to the process CWD and wrote .agentx transcripts
into the repo, dirtying the tree in CI and failing the build step's
worktree check.
Evidence: GOWORK=off go vet + go test -count=1 ./... green;
TestContractEmptyWorkspaceKeepsCWDClean pins the fixture bytes;
the adapters/native tree no longer drifts after a suite run.
Impact: requests with an explicit workspace are unchanged; empty
workspace now lands in a disposable temp dir.
Refs: Agent-System-Dev#242 CI failure (run 1144)
This commit is contained in:
2026-10-07 11:47:15 +00:00
parent 3a1ad5afc7
commit 4a7e19ad62
2 changed files with 55 additions and 1 deletions
+9 -1
View File
@@ -166,7 +166,15 @@ func (b *Backend) Execute(ctx context.Context, req exec.Request) (*exec.Result,
}
workDir := req.WorkDir
if !filepath.IsAbs(workDir) {
if strings.TrimSpace(workDir) == "" {
// 空工作区 = 请求未授权持久工作区:会话状态(.agentx/transcript 等)
// 落一次性临时目录,不污染进程 CWD(契约套件即发此类请求)。
tmp, err := os.MkdirTemp("", "agent-native-run-*")
if err != nil {
return &exec.Result{Success: false, ErrorReason: err.Error()}, nil
}
workDir = tmp
} else if !filepath.IsAbs(workDir) {
wd, _ := os.Getwd()
workDir = filepath.Join(wd, workDir)
}
+46
View File
@@ -2,6 +2,8 @@ package native
import (
"context"
"os"
"path/filepath"
"testing"
"time"
@@ -65,3 +67,47 @@ func TestContractDescriptor(t *testing.T) {
t.Fatalf("capabilities = %v", d.Capabilities)
}
}
// TestContractEmptyWorkspaceKeepsCWDClean 空 Workspace 的请求不得把会话状态
// 写进进程 CWD(CI 护栏:make test 后工作区必须保持干净;包目录的已提交
// .agentx 夹具内容必须逐字节不变)。
func TestContractEmptyWorkspaceKeepsCWDClean(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
snapshot := map[string][]byte{}
for _, rel := range []string{
filepath.Join(".agentx", "transcript.jsonl"),
filepath.Join(".agentx", "transcript.jsonl.meta.json"),
} {
if b, readErr := os.ReadFile(filepath.Join(wd, rel)); readErr == nil {
snapshot[rel] = b
}
}
b := New(Options{
ModelOptions: ModelOptions{
Model: "fake",
Stream: script(llm.Message{Role: llm.RoleAssistant, Content: "hi", StopReason: llm.StopEnd}),
},
}).Contract()
res, err := b.Execute(context.Background(), exec.ContractRequest{ExecutionID: "e-clean", Prompt: "hi"})
if err != nil {
t.Fatalf("transport error: %v", err)
}
if res.Outcome != exec.OutcomeSucceeded {
t.Fatalf("outcome = %q err = %+v", res.Outcome, res.Err)
}
for rel, want := range snapshot {
got, readErr := os.ReadFile(filepath.Join(wd, rel))
if readErr != nil {
t.Fatalf("夹具被删除:%s", rel)
}
if string(got) != string(want) {
t.Fatalf("空 Workspace 的执行改写了包内夹具 %s(应落临时目录)", rel)
}
}
if _, statErr := os.Stat(filepath.Join(wd, ".agentx", "activated-tools.json")); !os.IsNotExist(statErr) {
t.Fatal("空 Workspace 的执行在包目录写下了 activated-tools.json(应落临时目录)")
}
}