iop/packages/go/agentguard/admission_integration_test.go
toki 3c48879c44 feat(agent-runtime): 독립 Agent CLI 런타임 기반을 구현한다
독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
2026-07-29 13:12:21 +09:00

561 lines
18 KiB
Go

package agentguard
import (
"context"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
type admissionFixture struct {
request AdmissionRequest
baseRoot string
taskRoot string
commonGit string
gitDir string
}
func TestAdmissionS17Matrix(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink and worktree pointer fixtures require Unix semantics")
}
tests := []struct {
name string
mode IsolationMode
mutate func(*testing.T, *admissionFixture)
wantCode BlockerCode
wantInvoke int
}{
{
name: "registered full clone allowed",
mode: IsolationModeClone,
wantInvoke: 1,
},
{
name: "registered worktree with exact metadata allowance allowed",
mode: IsolationModeWorktree,
wantInvoke: 1,
},
{
name: "unregistered workspace",
mode: IsolationModeClone,
mutate: func(_ *testing.T, fixture *admissionFixture) {
fixture.request.Grant = nil
},
wantCode: BlockerCodeMissingWorkspaceGrant,
},
{
name: "worktree metadata allowance denied",
mode: IsolationModeWorktree,
mutate: func(_ *testing.T, fixture *admissionFixture) {
fixture.request.Grant.VCSMetadataRoots = nil
},
wantCode: BlockerCodeVCSMetadataNotAllowed,
},
{
name: "working directory symlink escape",
mode: IsolationModeClone,
mutate: func(t *testing.T, fixture *admissionFixture) {
outside := canonicalMkdir(t, filepath.Join(filepath.Dir(fixture.taskRoot), "outside"))
link := filepath.Join(fixture.taskRoot, "escape")
if err := os.Symlink(outside, link); err != nil {
t.Fatalf("Symlink: %v", err)
}
fixture.request.Isolation.WorkingDir = link
},
wantCode: BlockerCodeWorkspaceRootEscape,
},
{
name: "isolation cannot enforce writable roots",
mode: IsolationModeClone,
mutate: func(_ *testing.T, fixture *admissionFixture) {
fixture.request.Isolation.ConfinementRevision = ""
},
wantCode: BlockerCodeWritableConfinementUnavailable,
},
{
name: "profile cannot confine writable roots",
mode: IsolationModeClone,
mutate: func(_ *testing.T, fixture *admissionFixture) {
fixture.request.Profile.WritableRootConfinement = false
},
wantCode: BlockerCodeWritableConfinementUnavailable,
},
{
name: "provider unattended disabled",
mode: IsolationModeClone,
mutate: func(_ *testing.T, fixture *admissionFixture) {
fixture.request.Profile.Unattended = false
},
wantCode: BlockerCodeProviderUnattendedUnavailable,
},
{
name: "provider approval bypass disabled",
mode: IsolationModeClone,
mutate: func(_ *testing.T, fixture *admissionFixture) {
fixture.request.Profile.ApprovalBypass = false
},
wantCode: BlockerCodeProviderApprovalBypassUnavailable,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture := newAdmissionFixture(t, test.mode)
if test.mutate != nil {
test.mutate(t, &fixture)
}
invocations := 0
admission := Admit(fixture.request)
if test.wantCode != "" {
if admission.Allowed() || admission.Permit != nil ||
admission.Blocker == nil || admission.Blocker.Code != test.wantCode {
t.Fatalf("admission = %#v, want blocker %q", admission, test.wantCode)
}
if admission.Notification == nil || admission.Notification.SetupGuidance == "" {
t.Fatalf("notification = %#v", admission.Notification)
}
} else {
if !admission.Allowed() {
t.Fatalf("admission blocked: %#v", admission.Blocker)
}
invocationResult, err := Invoke(
context.Background(),
admission.Permit,
fixture.request,
func(_ context.Context, workspace CanonicalWorkspace) error {
invocations++
if workspace.WorkingDir != fixture.taskRoot {
t.Fatalf("working dir = %q, want %q", workspace.WorkingDir, fixture.taskRoot)
}
return nil
},
)
if err != nil || !invocationResult.Allowed() {
t.Fatalf("Invoke = result:%#v err:%v", invocationResult, err)
}
}
if invocations != test.wantInvoke {
t.Fatalf("invocations = %d, want %d", invocations, test.wantInvoke)
}
})
}
}
func TestAdmissionRejectsForgedStaleAndIdentityChangedPermitsWithoutInvocation(t *testing.T) {
t.Run("forged", func(t *testing.T) {
fixture := newAdmissionFixture(t, IsolationModeClone)
invocations := 0
result, err := Invoke(
context.Background(),
&Permit{},
fixture.request,
func(context.Context, CanonicalWorkspace) error {
invocations++
return nil
},
)
if err != nil || result.Blocker == nil || result.Blocker.Code != BlockerCodePermitInvalid {
t.Fatalf("Invoke forged = result:%#v err:%v", result, err)
}
if invocations != 0 {
t.Fatalf("forged permit invoked provider %d times", invocations)
}
})
t.Run("stale revision", func(t *testing.T) {
fixture := newAdmissionFixture(t, IsolationModeClone)
admission := Admit(fixture.request)
if !admission.Allowed() {
t.Fatalf("Admit: %#v", admission.Blocker)
}
fixture.request.Grant.Revision = "grant-r2"
invocations := 0
result, err := Invoke(
context.Background(),
admission.Permit,
fixture.request,
func(context.Context, CanonicalWorkspace) error {
invocations++
return nil
},
)
if err != nil || result.Blocker == nil || result.Blocker.Code != BlockerCodePermitStale {
t.Fatalf("Invoke stale = result:%#v err:%v", result, err)
}
if invocations != 0 {
t.Fatalf("stale permit invoked provider %d times", invocations)
}
})
t.Run("filesystem identity replacement", func(t *testing.T) {
fixture := newAdmissionFixture(t, IsolationModeClone)
admission := Admit(fixture.request)
if !admission.Allowed() {
t.Fatalf("Admit: %#v", admission.Blocker)
}
oldTaskRoot := fixture.taskRoot + "-old"
if err := os.Rename(fixture.taskRoot, oldTaskRoot); err != nil {
t.Fatalf("rename task root: %v", err)
}
canonicalMkdir(t, fixture.taskRoot)
canonicalMkdir(t, filepath.Join(fixture.taskRoot, ".git"))
invocations := 0
result, err := Invoke(
context.Background(),
admission.Permit,
fixture.request,
func(context.Context, CanonicalWorkspace) error {
invocations++
return nil
},
)
if err != nil || result.Blocker == nil ||
result.Blocker.Code != BlockerCodeWorkspaceIdentityMismatch {
t.Fatalf("Invoke replaced = result:%#v err:%v", result, err)
}
if invocations != 0 {
t.Fatalf("identity replacement invoked provider %d times", invocations)
}
})
}
func TestAdmissionCanonicalContainmentBoundaries(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink fixtures require Unix semantics")
}
tests := []struct {
name string
mutate func(*testing.T, *admissionFixture)
wantCode BlockerCode
}{
{
name: "relative task root",
mutate: func(_ *testing.T, fixture *admissionFixture) {
fixture.request.Isolation.TaskRoot = "relative/task"
},
wantCode: BlockerCodeWorkspaceNotCanonical,
},
{
name: "nonexistent task root",
mutate: func(_ *testing.T, fixture *admissionFixture) {
fixture.request.Isolation.TaskRoot = filepath.Join(filepath.Dir(fixture.taskRoot), "missing")
},
wantCode: BlockerCodeWorkspaceNotCanonical,
},
{
name: "parent traversal task root",
mutate: func(_ *testing.T, fixture *admissionFixture) {
fixture.request.Isolation.TaskRoot = fixture.taskRoot + "/../" + filepath.Base(fixture.taskRoot)
},
wantCode: BlockerCodeWorkspaceNotCanonical,
},
{
name: "canonical task root symlink",
mutate: func(t *testing.T, fixture *admissionFixture) {
link := fixture.taskRoot + "-link"
if err := os.Symlink(fixture.taskRoot, link); err != nil {
t.Fatalf("Symlink: %v", err)
}
fixture.request.Isolation.TaskRoot = link
},
wantCode: BlockerCodeWorkspaceNotCanonical,
},
{
name: "working file symlink",
mutate: func(t *testing.T, fixture *admissionFixture) {
file := filepath.Join(fixture.taskRoot, "file")
if err := os.WriteFile(file, []byte("fixture"), 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
link := filepath.Join(fixture.taskRoot, "file-link")
if err := os.Symlink(file, link); err != nil {
t.Fatalf("Symlink: %v", err)
}
fixture.request.Isolation.WorkingDir = link
},
wantCode: BlockerCodeWorkspaceRootEscape,
},
{
name: "prefix collision writable root",
mutate: func(t *testing.T, fixture *admissionFixture) {
sibling := canonicalMkdir(t, fixture.taskRoot+"-sibling")
fixture.request.Isolation.WritableRoots = []string{sibling}
},
wantCode: BlockerCodeWritableRootEscape,
},
{
name: "symlink loop",
mutate: func(t *testing.T, fixture *admissionFixture) {
a := filepath.Join(fixture.taskRoot, "loop-a")
b := filepath.Join(fixture.taskRoot, "loop-b")
if err := os.Symlink(b, a); err != nil {
t.Fatalf("symlink a: %v", err)
}
if err := os.Symlink(a, b); err != nil {
t.Fatalf("symlink b: %v", err)
}
fixture.request.Isolation.WorkingDir = a
},
wantCode: BlockerCodeWorkspaceRootEscape,
},
{
name: "clone git metadata symlink",
mutate: func(t *testing.T, fixture *admissionFixture) {
if err := os.RemoveAll(filepath.Join(fixture.taskRoot, ".git")); err != nil {
t.Fatalf("RemoveAll .git: %v", err)
}
externalGit := canonicalMkdir(t, filepath.Join(filepath.Dir(fixture.taskRoot), "external-git"))
if err := os.Symlink(externalGit, filepath.Join(fixture.taskRoot, ".git")); err != nil {
t.Fatalf("Symlink .git: %v", err)
}
},
wantCode: BlockerCodeVCSMetadataNotAllowed,
},
{
name: "base root revision mismatch",
mutate: func(_ *testing.T, fixture *admissionFixture) {
fixture.request.Isolation.BaseRoot = fixture.taskRoot
},
wantCode: BlockerCodeRevisionMismatch,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture := newAdmissionFixture(t, IsolationModeClone)
test.mutate(t, &fixture)
result := Admit(fixture.request)
if result.Blocker == nil || result.Blocker.Code != test.wantCode {
t.Fatalf("Admit = %#v, want %q", result, test.wantCode)
}
})
}
}
func TestAdmissionNestedWorkingRepositoryMetadata(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink and worktree pointer fixtures require Unix semantics")
}
t.Run("nested internal git directory allowed", func(t *testing.T) {
fixture := newAdmissionFixture(t, IsolationModeClone)
nestedDir := canonicalMkdir(t, filepath.Join(fixture.taskRoot, "nested"))
canonicalMkdir(t, filepath.Join(nestedDir, ".git"))
fixture.request.Isolation.WorkingDir = nestedDir
admission := Admit(fixture.request)
if !admission.Allowed() {
t.Fatalf("Admit = %#v, want allowed", admission.Blocker)
}
if admission.Workspace.WorkingDir != nestedDir {
t.Fatalf("working dir = %q, want %q", admission.Workspace.WorkingDir, nestedDir)
}
})
t.Run("nested external gitdir denied without allowance", func(t *testing.T) {
fixture := newAdmissionFixture(t, IsolationModeClone)
nestedDir := canonicalMkdir(t, filepath.Join(fixture.taskRoot, "nested"))
outsideGit := canonicalMkdir(t, filepath.Join(filepath.Dir(fixture.taskRoot), "outside-git"))
if err := os.WriteFile(
filepath.Join(nestedDir, ".git"),
[]byte("gitdir: "+outsideGit+"\n"),
0o600,
); err != nil {
t.Fatalf("write nested .git pointer: %v", err)
}
fixture.request.Isolation.WorkingDir = nestedDir
admission := Admit(fixture.request)
if admission.Allowed() || admission.Blocker == nil ||
admission.Blocker.Code != BlockerCodeVCSMetadataNotAllowed {
t.Fatalf("Admit = %#v, want %s", admission, BlockerCodeVCSMetadataNotAllowed)
}
if admission.Notification == nil || admission.Notification.SetupGuidance == "" {
t.Fatalf("notification = %#v", admission.Notification)
}
})
t.Run("nested external gitdir allowed with exact grant allowance", func(t *testing.T) {
fixture := newAdmissionFixture(t, IsolationModeClone)
nestedDir := canonicalMkdir(t, filepath.Join(fixture.taskRoot, "nested"))
outsideGit := canonicalMkdir(t, filepath.Join(filepath.Dir(fixture.taskRoot), "outside-git"))
if err := os.WriteFile(
filepath.Join(nestedDir, ".git"),
[]byte("gitdir: "+outsideGit+"\n"),
0o600,
); err != nil {
t.Fatalf("write nested .git pointer: %v", err)
}
fixture.request.Isolation.WorkingDir = nestedDir
fixture.request.Grant.VCSMetadataRoots = []string{outsideGit}
admission := Admit(fixture.request)
if !admission.Allowed() {
t.Fatalf("Admit = %#v, want allowed", admission.Blocker)
}
found := false
for _, root := range admission.Workspace.VCSMetadataRoots {
if root == outsideGit {
found = true
break
}
}
if !found {
t.Fatalf("VCSMetadataRoots = %v, want to contain %q", admission.Workspace.VCSMetadataRoots, outsideGit)
}
})
t.Run("nested symlink git entry denied", func(t *testing.T) {
fixture := newAdmissionFixture(t, IsolationModeClone)
nestedDir := canonicalMkdir(t, filepath.Join(fixture.taskRoot, "nested"))
outsideGit := canonicalMkdir(t, filepath.Join(filepath.Dir(fixture.taskRoot), "outside-git"))
if err := os.Symlink(outsideGit, filepath.Join(nestedDir, ".git")); err != nil {
t.Fatalf("Symlink nested .git: %v", err)
}
fixture.request.Isolation.WorkingDir = nestedDir
admission := Admit(fixture.request)
if admission.Allowed() || admission.Blocker == nil ||
admission.Blocker.Code != BlockerCodeVCSMetadataNotAllowed {
t.Fatalf("Admit = %#v, want %s", admission, BlockerCodeVCSMetadataNotAllowed)
}
})
}
func TestAdmissionAllowsSymlinkResolvedInsideWorkingDirectory(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink fixture requires Unix semantics")
}
fixture := newAdmissionFixture(t, IsolationModeClone)
inside := canonicalMkdir(t, filepath.Join(fixture.taskRoot, "inside"))
link := filepath.Join(fixture.taskRoot, "inside-link")
if err := os.Symlink(inside, link); err != nil {
t.Fatalf("Symlink: %v", err)
}
fixture.request.Isolation.WorkingDir = link
result := Admit(fixture.request)
if !result.Allowed() {
t.Fatalf("Admit: %#v", result.Blocker)
}
if result.Workspace.WorkingDir != inside {
t.Fatalf("working dir = %q, want %q", result.Workspace.WorkingDir, inside)
}
}
func TestBlockedProjectDoesNotStopIndependentProject(t *testing.T) {
blockedFixture := newAdmissionFixture(t, IsolationModeClone)
blockedFixture.request.Grant.ProjectID = "project-blocked"
blockedFixture.request.Profile.ApprovalBypass = false
allowedFixture := newAdmissionFixture(t, IsolationModeClone)
allowedFixture.request.Grant.ProjectID = "project-allowed"
invocations := map[string]int{}
for _, fixture := range []admissionFixture{blockedFixture, allowedFixture} {
admission := Admit(fixture.request)
if !admission.Allowed() {
continue
}
_, err := Invoke(
context.Background(),
admission.Permit,
fixture.request,
func(_ context.Context, workspace CanonicalWorkspace) error {
invocations[workspace.ProjectID]++
return nil
},
)
if err != nil {
t.Fatalf("Invoke: %v", err)
}
}
if invocations[blockedFixture.request.Grant.ProjectID] != 0 {
t.Fatalf("blocked project invocation count = %d", invocations[blockedFixture.request.Grant.ProjectID])
}
if invocations[allowedFixture.request.Grant.ProjectID] != 1 {
t.Fatalf("independent project invocation count = %d", invocations[allowedFixture.request.Grant.ProjectID])
}
}
func newAdmissionFixture(t *testing.T, mode IsolationMode) admissionFixture {
t.Helper()
root := canonicalTempDir(t)
baseRoot := canonicalMkdir(t, filepath.Join(root, "base-"+strings.ReplaceAll(t.Name(), "/", "-")))
taskRoot := canonicalMkdir(t, filepath.Join(root, "task-"+strings.ReplaceAll(t.Name(), "/", "-")))
fixture := admissionFixture{
baseRoot: baseRoot,
taskRoot: taskRoot,
request: AdmissionRequest{
Grant: &WorkspaceGrant{
ProjectID: "project-" + strings.ReplaceAll(t.Name(), "/", "-"),
WorkspaceID: "workspace-a",
Root: baseRoot,
Revision: "grant-r1",
},
Isolation: &IsolationDescriptor{
ID: "isolation-a",
Revision: "isolation-r1",
Mode: mode,
BaseRoot: baseRoot,
TaskRoot: taskRoot,
WorkingDir: taskRoot,
WritableRoots: []string{taskRoot},
PinnedBaseRevision: "base-r1",
ConfinementRevision: "confinement-r1",
},
Profile: ProviderProfile{
ProviderID: "codex",
ModelID: "gpt",
ProfileID: "codex-headless",
Revision: "profile-r1",
Unattended: true,
ApprovalBypass: true,
WritableRootConfinement: true,
},
},
}
switch mode {
case IsolationModeClone:
canonicalMkdir(t, filepath.Join(taskRoot, ".git"))
case IsolationModeWorktree:
commonGit := canonicalMkdir(t, filepath.Join(baseRoot, ".git"))
gitDir := canonicalMkdir(t, filepath.Join(commonGit, "worktrees", "task-a"))
if err := os.WriteFile(
filepath.Join(taskRoot, ".git"),
[]byte("gitdir: "+gitDir+"\n"),
0o600,
); err != nil {
t.Fatalf("write .git pointer: %v", err)
}
if err := os.WriteFile(filepath.Join(gitDir, "commondir"), []byte("../..\n"), 0o600); err != nil {
t.Fatalf("write commondir: %v", err)
}
fixture.commonGit = commonGit
fixture.gitDir = gitDir
fixture.request.Grant.VCSMetadataRoots = []string{gitDir, commonGit}
}
return fixture
}
func canonicalTempDir(t *testing.T) string {
t.Helper()
path, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatalf("EvalSymlinks temp dir: %v", err)
}
return filepath.Clean(path)
}
func canonicalMkdir(t *testing.T, path string) string {
t.Helper()
if err := os.MkdirAll(path, 0o700); err != nil {
t.Fatalf("MkdirAll %s: %v", path, err)
}
canonical, err := filepath.EvalSymlinks(path)
if err != nil {
t.Fatalf("EvalSymlinks %s: %v", path, err)
}
return filepath.Clean(canonical)
}