package agentworkspace import ( "context" "encoding/base64" "encoding/json" "errors" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "syscall" "testing" "time" "golang.org/x/sys/unix" "iop/packages/go/agentconfig" "iop/packages/go/agentguard" "iop/packages/go/agenttask" ) const confinementHelperEnvironment = "IOP_AGENTWORKSPACE_TEST_CONFINEMENT_HELPER" type confinementHelperPlan struct { Allowed []string `json:"allowed"` Denied []string `json:"denied"` AllowedMetadata []string `json:"allowed_metadata"` DeniedMetadata []string `json:"denied_metadata"` } func TestConfinementWriteHelper(t *testing.T) { encoded := os.Getenv(confinementHelperEnvironment) if encoded == "" { return } raw, err := base64.RawURLEncoding.DecodeString(encoded) if err != nil { t.Fatalf("decode helper plan: %v", err) } var plan confinementHelperPlan if err := json.Unmarshal(raw, &plan); err != nil { t.Fatalf("parse helper plan: %v", err) } for _, path := range plan.Allowed { if err := os.WriteFile(path, []byte("allowed\n"), 0o600); err != nil { t.Fatalf("allowed write %s: %v", path, err) } t.Logf("allowed write: %s", filepath.Base(path)) } for _, path := range plan.Denied { err := os.WriteFile(path, []byte("denied\n"), 0o600) if err == nil { t.Fatalf("denied write unexpectedly succeeded: %s", path) } if !errors.Is(err, syscall.EACCES) && !errors.Is(err, syscall.EPERM) && !errors.Is(err, syscall.EROFS) { t.Fatalf("denied write returned an unexpected error for %s: %v", path, err) } t.Logf("denied write: %s: %v", filepath.Base(path), err) } for _, path := range plan.AllowedMetadata { for _, mutation := range confinementMetadataMutations(path) { if err := mutation.apply(); err != nil { if mutation.name == "setxattr" && xattrUnsupported(err) { t.Logf("allowed metadata %s unsupported: %v", mutation.name, err) continue } t.Fatalf("allowed metadata %s %s: %v", mutation.name, path, err) } t.Logf("allowed metadata %s: %s", mutation.name, filepath.Base(path)) } } for _, path := range plan.DeniedMetadata { for _, mutation := range confinementMetadataMutations(path) { err := mutation.apply() if mutation.name == "setxattr" && xattrUnsupported(err) { t.Logf("denied metadata %s unsupported: %v", mutation.name, err) continue } if err == nil { t.Fatalf("denied metadata %s unexpectedly succeeded: %s", mutation.name, path) } if !confinementMutationDenied(err) { t.Fatalf( "denied metadata %s returned an unexpected error for %s: %v", mutation.name, path, err, ) } t.Logf("denied metadata %s: %s: %v", mutation.name, filepath.Base(path), err) } } } type confinementMetadataMutation struct { name string apply func() error } func confinementMetadataMutations(path string) []confinementMetadataMutation { timestamp := time.Unix(123456789, 0) return []confinementMetadataMutation{ {name: "chmod", apply: func() error { return os.Chmod(path, 0o640) }}, {name: "utime", apply: func() error { return os.Chtimes(path, timestamp, timestamp) }}, {name: "chown", apply: func() error { return os.Chown(path, os.Getuid(), os.Getgid()) }}, {name: "setxattr", apply: func() error { return unix.Setxattr(path, "user.iop_agentworkspace_test", []byte("metadata"), 0) }}, } } func confinementMutationDenied(err error) bool { return errors.Is(err, syscall.EACCES) || errors.Is(err, syscall.EPERM) || errors.Is(err, syscall.EROFS) } func xattrUnsupported(err error) bool { return errors.Is(err, unix.ENOTSUP) || errors.Is(err, unix.EOPNOTSUPP) } func TestOverlayBackendConfinementDeniesActualAbsoluteWrites(t *testing.T) { if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { t.Skip("executable confinement is implemented on Linux and macOS") } baseRoot, localRoot := newWorkspaceFixture(t) initializeDirtyGitFixture(t, baseRoot) protectedDescriptorPath := filepath.Join(baseRoot, "descriptor-protected.txt") writeFile(t, protectedDescriptorPath, "descriptor protected\n", 0o600) beforeSnapshot, err := captureWorkspaceSnapshot( context.Background(), baseRoot, "", "config-r1", "grant-r1", ) if err != nil { t.Fatalf("capture before snapshot: %v", err) } beforeStatus := gitOutput(t, baseRoot, "status", "--porcelain=v1", "--untracked-files=all") beforeGitConfig, err := os.ReadFile(filepath.Join(baseRoot, ".git", "config")) if err != nil { t.Fatalf("read canonical Git config: %v", err) } backend := newTestBackend(t, localRoot, baseRoot) first, err := backend.Prepare( context.Background(), testIsolationRequest("task-a", "task-a#1"), ) if err != nil { t.Fatalf("Prepare(first): %v", err) } second, err := backend.Prepare( context.Background(), testIsolationRequest("task-b", "task-b#1"), ) if err != nil { t.Fatalf("Prepare(second): %v", err) } firstRecord, err := backend.LoadRecord(*first.Descriptor) if err != nil { t.Fatalf("LoadRecord(first): %v", err) } secondRecord, err := backend.LoadRecord(*second.Descriptor) if err != nil { t.Fatalf("LoadRecord(second): %v", err) } beforeSnapshotRecord, err := os.ReadFile(firstRecord.Locator.SnapshotRecord) if err != nil { t.Fatalf("read snapshot record: %v", err) } allowed := []string{ filepath.Join(firstRecord.Locator.ViewRoot, "child-view.txt"), filepath.Join(firstRecord.Locator.TempRoot, "child-temp.txt"), filepath.Join(firstRecord.Locator.CacheRoot, "child-cache.txt"), } denied := []string{ filepath.Join(baseRoot, "canonical-child.txt"), protectedDescriptorPath, filepath.Join(secondRecord.Locator.ViewRoot, "sibling-child.txt"), filepath.Join(firstRecord.Locator.SnapshotRoot, "snapshot-child.txt"), filepath.Join(baseRoot, ".git", "confinement-child"), } deniedMetadata := []string{ filepath.Join(baseRoot, "shared.txt"), protectedDescriptorPath, filepath.Join(secondRecord.Locator.ViewRoot, "shared.txt"), firstRecord.Locator.SnapshotRecord, firstRecord.Locator.OverlayRecord, filepath.Join(baseRoot, ".git", "config"), } beforeMetadata := make(map[string]fileMetadata, len(deniedMetadata)) for _, path := range deniedMetadata { beforeMetadata[path] = captureFileMetadata(t, path) } encodedPlan, err := json.Marshal(confinementHelperPlan{ Allowed: allowed, Denied: denied, AllowedMetadata: allowed, DeniedMetadata: deniedMetadata, }) if err != nil { t.Fatalf("encode helper plan: %v", err) } protectedDescriptor, err := os.OpenFile( protectedDescriptorPath, os.O_WRONLY|os.O_APPEND, 0, ) if err != nil { t.Fatalf("open protected host descriptor: %v", err) } defer protectedDescriptor.Close() started, err := first.Confinement.Start( context.Background(), agenttask.ConfinementCommand{ Name: os.Args[0], Args: []string{ "-test.run=^TestConfinementWriteHelper$", "-test.v", }, Env: append( os.Environ(), confinementHelperEnvironment+"="+ base64.RawURLEncoding.EncodeToString(encodedPlan), ), }, ) if err != nil { t.Fatalf("start confined child: %v", err) } stdoutFile, stdoutIsFile := started.Stdout().(*os.File) stderrFile, stderrIsFile := started.Stderr().(*os.File) if !stdoutIsFile || !stderrIsFile || stdoutFile.Fd() == protectedDescriptor.Fd() || stderrFile.Fd() == protectedDescriptor.Fd() { t.Fatal("proof-owned child output reused the protected host descriptor") } type pipeResult struct { content string err error } stdoutResult := make(chan pipeResult, 1) stderrResult := make(chan pipeResult, 1) go func() { content, err := io.ReadAll(started.Stdout()) stdoutResult <- pipeResult{content: string(content), err: err} }() go func() { content, err := io.ReadAll(started.Stderr()) stderrResult <- pipeResult{content: string(content), err: err} }() if err := started.Stdin().Close(); err != nil { t.Fatalf("close confined child stdin: %v", err) } waitErr := started.Child().Wait() stdout := <-stdoutResult stderr := <-stderrResult if stdout.err != nil || stderr.err != nil { t.Fatalf("read confined helper output: stdout=%v stderr=%v", stdout.err, stderr.err) } output := stdout.content + stderr.content if waitErr != nil { t.Fatalf("confined helper: %v\n%s", waitErr, output) } if err := started.Abort(); err != nil { t.Fatalf("cleanup confined helper: %v", err) } t.Logf("confined child output:\n%s", output) if !strings.Contains(output, "allowed write") || !strings.Contains(output, "denied write") || !strings.Contains(output, "allowed metadata") || !strings.Contains(output, "denied metadata") { t.Fatalf("confined helper did not report both outcomes:\n%s", output) } for _, path := range allowed { assertFileContent(t, path, "allowed\n") } for _, path := range denied { if path == protectedDescriptorPath { assertFileContent(t, path, "descriptor protected\n") continue } assertNotExist(t, path) } for path, before := range beforeMetadata { if after := captureFileMetadata(t, path); after != before { t.Fatalf("confined child changed protected metadata at %s: before=%#v after=%#v", path, before, after) } } afterSnapshotRecord, err := os.ReadFile(firstRecord.Locator.SnapshotRecord) if err != nil { t.Fatalf("read snapshot record after child: %v", err) } if string(afterSnapshotRecord) != string(beforeSnapshotRecord) { t.Fatal("confined child changed the immutable snapshot record") } afterGitConfig, err := os.ReadFile(filepath.Join(baseRoot, ".git", "config")) if err != nil { t.Fatalf("read canonical Git config after child: %v", err) } if string(afterGitConfig) != string(beforeGitConfig) { t.Fatal("confined child changed shared Git metadata") } afterStatus := gitOutput(t, baseRoot, "status", "--porcelain=v1", "--untracked-files=all") if afterStatus != beforeStatus { t.Fatalf("canonical Git status changed:\nbefore=%q\nafter=%q", beforeStatus, afterStatus) } afterSnapshot, err := captureWorkspaceSnapshot( context.Background(), baseRoot, "", "config-r1", "grant-r1", ) if err != nil { t.Fatalf("capture after snapshot: %v", err) } if afterSnapshot.Revision != beforeSnapshot.Revision { t.Fatalf( "canonical fingerprint changed: %q != %q", afterSnapshot.Revision, beforeSnapshot.Revision, ) } } type fileMetadata struct { Mode os.FileMode ModTime time.Time } func captureFileMetadata(t *testing.T, path string) fileMetadata { t.Helper() info, err := os.Lstat(path) if err != nil { t.Fatalf("stat %s: %v", path, err) } return fileMetadata{Mode: info.Mode(), ModTime: info.ModTime()} } func TestOverlayBackendConcurrentTasksSharePinnedDirtyBaseAndIsolateWrites(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("mode and symlink fixtures require Unix filesystem semantics") } baseRoot, localRoot := newWorkspaceFixture(t) initializeDirtyGitFixture(t, baseRoot) beforeStatus := gitOutput(t, baseRoot, "status", "--porcelain=v1", "--untracked-files=all") backend := newTestBackend(t, localRoot, baseRoot) requests := []agenttask.IsolationRequest{ testIsolationRequest("task-a", "task-a#1"), testIsolationRequest("task-b", "task-b#1"), } prepared := make([]agenttask.PreparedIsolation, len(requests)) errorsByIndex := make([]error, len(requests)) var wait sync.WaitGroup for index := range requests { index := index wait.Add(1) go func() { defer wait.Done() prepared[index], errorsByIndex[index] = backend.Prepare( context.Background(), requests[index], ) }() } wait.Wait() for index, err := range errorsByIndex { if err != nil { t.Fatalf("Prepare task %d: %v", index, err) } } first := prepared[0].Descriptor second := prepared[1].Descriptor if first.PinnedBaseRevision != second.PinnedBaseRevision { t.Fatalf( "pinned base revisions differ: %q != %q", first.PinnedBaseRevision, second.PinnedBaseRevision, ) } if first.TaskRoot == second.TaskRoot || first.WorkingDir == second.WorkingDir { t.Fatalf("task roots are not isolated: %#v %#v", first, second) } for _, result := range prepared { admission := agentguard.Admit(agentguard.AdmissionRequest{ Grant: result.Grant, Isolation: result.Descriptor, Profile: result.Profile, }) if !admission.Allowed() { t.Fatalf("prepared overlay was not admissible: %#v", admission.Blocker) } } firstRecord, err := backend.LoadRecord(*first) if err != nil { t.Fatalf("LoadRecord(first): %v", err) } secondRecord, err := backend.LoadRecord(*second) if err != nil { t.Fatalf("LoadRecord(second): %v", err) } if firstRecord.Locator.SnapshotRoot != secondRecord.Locator.SnapshotRoot { t.Fatalf( "snapshot roots differ: %q != %q", firstRecord.Locator.SnapshotRoot, secondRecord.Locator.SnapshotRoot, ) } assertSnapshotEvidence(t, firstRecord.Locator.SnapshotRecord) assertFileContent(t, filepath.Join(first.WorkingDir, "shared.txt"), "dirty base\n") assertFileContent(t, filepath.Join(second.WorkingDir, "shared.txt"), "dirty base\n") assertFileContent(t, filepath.Join(first.WorkingDir, "untracked.txt"), "untracked base\n") assertFileContent(t, filepath.Join(second.WorkingDir, "untracked.txt"), "untracked base\n") assertSymlink(t, filepath.Join(first.WorkingDir, "shared-link"), "shared.txt") assertExecutable(t, filepath.Join(first.WorkingDir, "script.sh")) writeFile(t, filepath.Join(first.WorkingDir, "shared.txt"), "task a\n", 0o644) writeFile(t, filepath.Join(second.WorkingDir, "shared.txt"), "task b\n", 0o644) writeFile(t, filepath.Join(first.WorkingDir, "only-a.txt"), "a\n", 0o644) writeFile(t, filepath.Join(second.WorkingDir, "only-b.txt"), "b\n", 0o644) writeFile(t, filepath.Join(firstRecord.Locator.TempRoot, "build.tmp"), "a temp\n", 0o600) writeFile(t, filepath.Join(secondRecord.Locator.TempRoot, "build.tmp"), "b temp\n", 0o600) writeFile(t, filepath.Join(firstRecord.Locator.CacheRoot, "cache"), "a cache\n", 0o600) writeFile(t, filepath.Join(secondRecord.Locator.CacheRoot, "cache"), "b cache\n", 0o600) writeFile(t, filepath.Join(firstRecord.Locator.GitMetadata, "task-owned"), "a git\n", 0o600) assertFileContent(t, filepath.Join(first.WorkingDir, "shared.txt"), "task a\n") assertFileContent(t, filepath.Join(second.WorkingDir, "shared.txt"), "task b\n") assertFileContent(t, filepath.Join(baseRoot, "shared.txt"), "dirty base\n") assertNotExist(t, filepath.Join(baseRoot, "only-a.txt")) assertNotExist(t, filepath.Join(baseRoot, "only-b.txt")) assertNotExist(t, filepath.Join(second.WorkingDir, "only-a.txt")) assertNotExist(t, filepath.Join(first.WorkingDir, "only-b.txt")) assertFileContent(t, filepath.Join(firstRecord.Locator.TempRoot, "build.tmp"), "a temp\n") assertFileContent(t, filepath.Join(secondRecord.Locator.TempRoot, "build.tmp"), "b temp\n") assertFileContent(t, filepath.Join(firstRecord.Locator.CacheRoot, "cache"), "a cache\n") assertFileContent(t, filepath.Join(secondRecord.Locator.CacheRoot, "cache"), "b cache\n") assertNotExist(t, filepath.Join(baseRoot, ".git", "task-owned")) assertNotExist(t, filepath.Join(secondRecord.Locator.GitMetadata, "task-owned")) afterStatus := gitOutput(t, baseRoot, "status", "--porcelain=v1", "--untracked-files=all") if beforeStatus != afterStatus { t.Fatalf("canonical Git state changed:\nbefore=%q\nafter=%q", beforeStatus, afterStatus) } assertWritableRootDenied(t, prepared[0], baseRoot, agentguard.BlockerCodeWritableRootEscape) assertWritableRootDenied( t, prepared[0], secondRecord.Locator.ViewRoot, agentguard.BlockerCodeWritableRootEscape, ) canonicalLink := filepath.Join(firstRecord.Locator.TempRoot, "canonical-link") if err := os.Symlink(baseRoot, canonicalLink); err != nil { t.Fatalf("create canonical link: %v", err) } assertWritableRootDenied( t, prepared[0], canonicalLink, agentguard.BlockerCodeWritableRootEscape, ) } func TestOverlayBackendIdempotencyAndFailureRetention(t *testing.T) { baseRoot, localRoot := newWorkspaceFixture(t) writeFile(t, filepath.Join(baseRoot, "input.txt"), "base\n", 0o644) backend := newTestBackend(t, localRoot, baseRoot) request := testIsolationRequest("task", "task#1") first, err := backend.Prepare(context.Background(), request) if err != nil { t.Fatalf("Prepare(first): %v", err) } writeFile(t, filepath.Join(first.Descriptor.WorkingDir, "partial.txt"), "retained\n", 0o600) writeFile(t, filepath.Join(baseRoot, "input.txt"), "base drift\n", 0o644) second, err := backend.Prepare(context.Background(), request) if err != nil { t.Fatalf("Prepare(second): %v", err) } if first.Descriptor.ID != second.Descriptor.ID || first.Descriptor.Revision != second.Descriptor.Revision || first.Descriptor.TaskRoot != second.Descriptor.TaskRoot { t.Fatalf("idempotent prepare changed identity: %#v != %#v", first.Descriptor, second.Descriptor) } assertFileContent(t, filepath.Join(second.Descriptor.WorkingDir, "input.txt"), "base\n") conflicting := request conflicting.Work.Unit.ID = "other-task" conflicting.Work.AttemptID = "other-task#1" if _, err := backend.Prepare(context.Background(), conflicting); err == nil || !strings.Contains(err.Error(), "does not match the request") { t.Fatalf("conflicting idempotency key error = %v", err) } freshRequest := testIsolationRequest("fresh-task", "fresh-task#1") fresh, err := backend.Prepare(context.Background(), freshRequest) if err != nil { t.Fatalf("Prepare(fresh): %v", err) } if fresh.Descriptor.PinnedBaseRevision == first.Descriptor.PinnedBaseRevision { t.Fatal("fresh task reused a stale base revision") } assertFileContent(t, filepath.Join(fresh.Descriptor.WorkingDir, "input.txt"), "base drift\n") blockedProfile := second.Profile blockedProfile.ApprovalBypass = false admission := agentguard.Admit(agentguard.AdmissionRequest{ Grant: second.Grant, Isolation: second.Descriptor, Profile: blockedProfile, }) if admission.Allowed() || admission.Blocker == nil || admission.Blocker.Code != agentguard.BlockerCodeProviderApprovalBypassUnavailable { t.Fatalf("unexpected blocked admission: %#v", admission) } record, err := backend.LoadRecord(*second.Descriptor) if err != nil { t.Fatalf("LoadRecord after admission failure: %v", err) } if record.Retention != RetentionStateActive { t.Fatalf("retention state = %q, want %q", record.Retention, RetentionStateActive) } if record.RetentionPolicy.BlockedDays != 14 { t.Fatalf("blocked retention days = %d, want 14", record.RetentionPolicy.BlockedDays) } assertFileContent(t, filepath.Join(second.Descriptor.WorkingDir, "partial.txt"), "retained\n") assertFileContent(t, filepath.Join(baseRoot, "input.txt"), "base drift\n") } func TestOverlayBackendRejectsRetainedIdentityRebinding(t *testing.T) { baseRootA, localRoot := newWorkspaceFixture(t) baseRootB := filepath.Join(filepath.Dir(baseRootA), "workspace-b") if err := os.MkdirAll(baseRootB, 0o700); err != nil { t.Fatalf("create root B: %v", err) } writeFile(t, filepath.Join(baseRootA, "identity.txt"), "root A\n", 0o600) writeFile(t, filepath.Join(baseRootB, "identity.txt"), "root B\n", 0o600) resolvedRoot := baseRootA resolver := InputResolverFunc(func( _ context.Context, request agenttask.IsolationRequest, ) (ResolvedInputs, error) { return ResolvedInputs{ Grant: agentguard.WorkspaceGrant{ ProjectID: string(request.Project.ProjectID), WorkspaceID: string(request.Project.WorkspaceID), Root: resolvedRoot, Revision: string(request.Project.Intent.GrantRevision), }, Profile: agentguard.ProviderProfile{ ProviderID: request.Target.ProviderID, ModelID: request.Target.ModelID, ProfileID: request.Target.ProfileID, Revision: request.Target.ProfileRevision, Unattended: true, ApprovalBypass: true, WritableRootConfinement: true, }, }, nil }) backend, err := NewBackend(BackendConfig{LocalRoot: localRoot}, resolver) if err != nil { t.Fatalf("NewBackend: %v", err) } request := testIsolationRequest("task", "task#1") first, err := backend.Prepare(context.Background(), request) if err != nil { t.Fatalf("Prepare(first): %v", err) } record, err := backend.LoadRecord(*first.Descriptor) if err != nil { t.Fatalf("LoadRecord(first): %v", err) } beforeRecord, err := os.ReadFile(record.Locator.OverlayRecord) if err != nil { t.Fatalf("read retained record: %v", err) } snapshot, err := readWorkspaceSnapshot(record.Locator.SnapshotRecord) if err != nil { t.Fatalf("read retained snapshot: %v", err) } if snapshot.CanonicalRoot != baseRootA || snapshot.ConfigRevision != "config-r1" || snapshot.GrantRevision != "grant-r1" { t.Fatalf("snapshot identity = %#v", snapshot) } resolvedRoot = baseRootB if _, err := backend.Prepare(context.Background(), request); err == nil || !strings.Contains(err.Error(), "identity does not match") { t.Fatalf("root rebinding error = %v", err) } resolvedRoot = baseRootA configRebind := request configIntent := *request.Project.Intent configIntent.ConfigRevision = "config-r2" configRebind.Project.Intent = &configIntent configRebind.Target.ConfigRevision = "config-r2" if _, err := backend.Prepare(context.Background(), configRebind); err == nil || !strings.Contains(err.Error(), "identity does not match") { t.Fatalf("config rebinding error = %v", err) } grantRebind := request grantIntent := *request.Project.Intent grantIntent.GrantRevision = "grant-r2" grantRebind.Project.Intent = &grantIntent if _, err := backend.Prepare(context.Background(), grantRebind); err == nil || !strings.Contains(err.Error(), "identity does not match") { t.Fatalf("grant rebinding error = %v", err) } afterRecord, err := os.ReadFile(record.Locator.OverlayRecord) if err != nil { t.Fatalf("read retained record after rejection: %v", err) } if string(afterRecord) != string(beforeRecord) { t.Fatal("identity rebinding changed the retained overlay record") } exact, err := backend.Prepare(context.Background(), request) if err != nil { t.Fatalf("Prepare(exact replay): %v", err) } if exact.Descriptor.ID != first.Descriptor.ID || exact.Descriptor.Revision != first.Descriptor.Revision || exact.Descriptor.ConfinementRevision != first.Descriptor.ConfinementRevision { t.Fatalf("exact replay changed identity: %#v != %#v", exact.Descriptor, first.Descriptor) } assertFileContent(t, filepath.Join(exact.Descriptor.WorkingDir, "identity.txt"), "root A\n") } func TestOverlayBackendRejectsCanonicalSymlinkEscapeBeforeCreatingTask(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("symlink fixture requires Unix filesystem semantics") } baseRoot, localRoot := newWorkspaceFixture(t) outside := filepath.Join(filepath.Dir(baseRoot), "outside.txt") writeFile(t, outside, "outside\n", 0o600) if err := os.Symlink(outside, filepath.Join(baseRoot, "escape")); err != nil { t.Fatalf("create escaping symlink: %v", err) } backend := newTestBackend(t, localRoot, baseRoot) _, err := backend.Prepare( context.Background(), testIsolationRequest("task", "task#1"), ) if err == nil || !strings.Contains(err.Error(), "absolute symlink") { t.Fatalf("Prepare error = %v, want absolute symlink confinement failure", err) } entries, readErr := os.ReadDir(filepath.Join(localRoot, "tasks")) if readErr != nil { t.Fatalf("ReadDir tasks: %v", readErr) } if len(entries) != 0 { t.Fatalf("failed preparation left task artifacts: %v", entries) } } func TestOverlayBackendRejectsNestedSharedGitMetadata(t *testing.T) { baseRoot, localRoot := newWorkspaceFixture(t) writeFile(t, filepath.Join(baseRoot, "nested", ".git", "config"), "[core]\n", 0o600) backend := newTestBackend(t, localRoot, baseRoot) _, err := backend.Prepare( context.Background(), testIsolationRequest("task", "task#1"), ) if err == nil || !strings.Contains(err.Error(), "nested Git metadata") { t.Fatalf("Prepare error = %v, want nested Git metadata failure", err) } entries, readErr := os.ReadDir(filepath.Join(localRoot, "tasks")) if readErr != nil { t.Fatalf("ReadDir tasks: %v", readErr) } if len(entries) != 0 { t.Fatalf("failed preparation left task artifacts: %v", entries) } } func newTestBackend(t *testing.T, localRoot, baseRoot string) *Backend { t.Helper() resolver := InputResolverFunc(func( _ context.Context, request agenttask.IsolationRequest, ) (ResolvedInputs, error) { return ResolvedInputs{ Grant: agentguard.WorkspaceGrant{ ProjectID: string(request.Project.ProjectID), WorkspaceID: string(request.Project.WorkspaceID), Root: baseRoot, Revision: string(request.Project.Intent.GrantRevision), }, Profile: agentguard.ProviderProfile{ ProviderID: request.Target.ProviderID, ModelID: request.Target.ModelID, ProfileID: request.Target.ProfileID, Revision: request.Target.ProfileRevision, Unattended: true, ApprovalBypass: true, WritableRootConfinement: true, }, }, nil }) backend, err := NewBackend(BackendConfig{ LocalRoot: localRoot, Retention: agentconfig.RetentionPolicy{ CompletedDays: 7, BlockedDays: 14, }, }, resolver) if err != nil { t.Fatalf("NewBackend: %v", err) } return backend } func testIsolationRequest(workID agenttask.WorkUnitID, attemptID agenttask.AttemptID) agenttask.IsolationRequest { projectID := agenttask.ProjectID("project") workspaceID := agenttask.WorkspaceID("workspace") return agenttask.IsolationRequest{ Project: agenttask.ProjectRecord{ ProjectID: projectID, WorkspaceID: workspaceID, Intent: &agenttask.StartIntent{ ProjectID: projectID, WorkspaceID: workspaceID, GrantRevision: "grant-r1", ConfigRevision: "config-r1", }, }, Work: agenttask.WorkRecord{ Unit: agenttask.WorkUnit{ ID: workID, IsolationMode: agentguard.IsolationModeOverlay, }, AttemptID: attemptID, }, Target: agenttask.ExecutionTarget{ ProviderID: "provider", ModelID: "model", ProfileID: "profile", ProfileRevision: "profile-r1", ConfigRevision: "config-r1", Capacity: 2, }, IdempotencyKey: "dispatch/" + string(workID) + "/" + string(attemptID) + "/isolation", } } func newWorkspaceFixture(t *testing.T) (string, string) { t.Helper() root, err := filepath.EvalSymlinks(t.TempDir()) if err != nil { t.Fatalf("EvalSymlinks: %v", err) } baseRoot := filepath.Join(root, "workspace") localRoot := filepath.Join(root, "runtime") for _, directory := range []string{baseRoot, localRoot} { if err := os.MkdirAll(directory, 0o700); err != nil { t.Fatalf("MkdirAll %s: %v", directory, err) } } return baseRoot, localRoot } func initializeDirtyGitFixture(t *testing.T, root string) { t.Helper() if _, err := exec.LookPath("git"); err != nil { t.Skip("git is required for the workspace snapshot fixture") } gitRun(t, root, "init", "-q") gitRun(t, root, "config", "user.email", "agentworkspace@example.invalid") gitRun(t, root, "config", "user.name", "Agent Workspace Test") writeFile(t, filepath.Join(root, "shared.txt"), "committed\n", 0o644) writeFile(t, filepath.Join(root, "script.sh"), "#!/bin/sh\nexit 0\n", 0o755) if err := os.Symlink("shared.txt", filepath.Join(root, "shared-link")); err != nil { t.Fatalf("create internal symlink: %v", err) } gitRun(t, root, "add", "shared.txt", "script.sh", "shared-link") gitRun(t, root, "commit", "-q", "-m", "fixture") writeFile(t, filepath.Join(root, "shared.txt"), "dirty base\n", 0o644) writeFile(t, filepath.Join(root, "untracked.txt"), "untracked base\n", 0o644) } func assertSnapshotEvidence(t *testing.T, recordPath string) { t.Helper() snapshot, err := readWorkspaceSnapshot(recordPath) if err != nil { t.Fatalf("readWorkspaceSnapshot: %v", err) } entries := make(map[string]SnapshotEntry, len(snapshot.Entries)) for _, entry := range snapshot.Entries { entries[entry.Path] = entry } shared := entries["shared.txt"] if shared.GitState != SnapshotGitStateTracked || !shared.Dirty || shared.ContentDigest == "" { t.Fatalf("tracked dirty entry = %#v", shared) } untracked := entries["untracked.txt"] if untracked.GitState != SnapshotGitStateUntracked || untracked.Dirty || untracked.ContentDigest == "" { t.Fatalf("untracked entry = %#v", untracked) } script := entries["script.sh"] if os.FileMode(script.Mode).Perm()&0o100 == 0 { t.Fatalf("script mode was not fingerprinted: %#o", script.Mode) } link := entries["shared-link"] if link.Kind != SnapshotEntrySymlink || link.SymlinkTarget != "shared.txt" || link.ContentDigest == "" { t.Fatalf("symlink entry = %#v", link) } } func assertWritableRootDenied( t *testing.T, prepared agenttask.PreparedIsolation, root string, want agentguard.BlockerCode, ) { t.Helper() descriptor := *prepared.Descriptor descriptor.WritableRoots = append(append([]string(nil), descriptor.WritableRoots...), root) result := agentguard.Admit(agentguard.AdmissionRequest{ Grant: prepared.Grant, Isolation: &descriptor, Profile: prepared.Profile, }) if result.Allowed() || result.Blocker == nil || result.Blocker.Code != want { t.Fatalf("tampered writable root result = %#v, want %q", result, want) } } func assertFileContent(t *testing.T, path, want string) { t.Helper() content, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile %s: %v", path, err) } if string(content) != want { t.Fatalf("content %s = %q, want %q", path, content, want) } } func assertNotExist(t *testing.T, path string) { t.Helper() if _, err := os.Lstat(path); !os.IsNotExist(err) { t.Fatalf("path %s exists or returned unexpected error: %v", path, err) } } func assertSymlink(t *testing.T, path, want string) { t.Helper() target, err := os.Readlink(path) if err != nil { t.Fatalf("Readlink %s: %v", path, err) } if target != want { t.Fatalf("symlink target %s = %q, want %q", path, target, want) } } func assertExecutable(t *testing.T, path string) { t.Helper() info, err := os.Stat(path) if err != nil { t.Fatalf("Stat %s: %v", path, err) } if info.Mode().Perm()&0o100 == 0 { t.Fatalf("%s is not executable: %s", path, info.Mode()) } } func writeFile(t *testing.T, path, content string, mode os.FileMode) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatalf("MkdirAll %s: %v", filepath.Dir(path), err) } if err := os.WriteFile(path, []byte(content), mode); err != nil { t.Fatalf("WriteFile %s: %v", path, err) } if err := os.Chmod(path, mode); err != nil { t.Fatalf("Chmod %s: %v", path, err) } } func gitRun(t *testing.T, root string, args ...string) { t.Helper() command := exec.Command("git", append([]string{"-C", root}, args...)...) command.Env = append(os.Environ(), "LC_ALL=C") if output, err := command.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, output) } } func gitOutput(t *testing.T, root string, args ...string) string { t.Helper() command := exec.Command("git", append([]string{"-C", root}, args...)...) command.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0", "LC_ALL=C") output, err := command.CombinedOutput() if err != nil { t.Fatalf("git %v: %v\n%s", args, err, output) } return string(output) }