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

208 lines
5.8 KiB
Go

package agentworkspace
import (
"context"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"iop/packages/go/agenttask"
)
func TestConfinementPlatformFailsWithTypedUnavailableError(t *testing.T) {
identity, err := platformConfinementRevision()
if err != nil {
if !errors.Is(err, ErrConfinementUnavailable) {
t.Fatalf("platform error = %v, want ErrConfinementUnavailable", err)
}
return
}
if identity == "" {
t.Fatal("available confinement returned an empty platform identity")
}
}
func TestConfinementLinuxUsesMetadataCompleteMountNamespacePolicy(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("Linux-only confinement policy")
}
identity, err := platformConfinementRevision()
if err != nil {
if errors.Is(err, ErrConfinementUnavailable) {
return
}
t.Fatalf("platform confinement revision: %v", err)
}
if !strings.Contains(identity, "mount-namespace-metadata") || strings.Contains(identity, "landlock") {
t.Fatalf("Linux confinement identity = %q, want metadata-complete mount namespace policy", identity)
}
}
func TestConfinementProofRejectsTamperedBinding(t *testing.T) {
baseRoot, localRoot := newWorkspaceFixture(t)
writeFile(t, filepath.Join(baseRoot, "input.txt"), "base\n", 0o600)
backend := newTestBackend(t, localRoot, baseRoot)
prepared, err := backend.Prepare(
context.Background(),
testIsolationRequest("task", "task#1"),
)
if err != nil {
t.Fatalf("Prepare: %v", err)
}
proof, ok := prepared.Confinement.(*ConfinementProof)
if !ok {
t.Fatalf("confinement proof type = %T", prepared.Confinement)
}
expected := proof.Binding()
expected.ProfileRevision = "profile-r2"
if err := proof.Validate(expected); err == nil {
t.Fatal("profile rebinding passed confinement validation")
}
tampered := *proof
tampered.binding = proof.Binding()
tampered.binding.BaseRoot = filepath.Dir(baseRoot)
if _, err := tampered.Start(
context.Background(),
agenttask.ConfinementCommand{Name: "true"},
); err == nil {
t.Fatal("tampered proof produced a child command")
}
}
func TestConfinementStartCreatesProofOwnedPipes(t *testing.T) {
started, err := startConfinementCommand(exec.Command(
"sh",
"-c",
`read line; printf 'stdout:%s\n' "$line"; printf 'stderr:%s\n' "$line" >&2`,
))
if err != nil {
t.Fatalf("startConfinementCommand: %v", err)
}
if started.Child() == nil ||
started.Stdin() == nil ||
started.Stdout() == nil ||
started.Stderr() == nil {
t.Fatalf("started handle is incomplete: %#v", started)
}
type readResult struct {
content string
err error
}
stdoutResult := make(chan readResult, 1)
stderrResult := make(chan readResult, 1)
go func() {
content, err := io.ReadAll(started.Stdout())
stdoutResult <- readResult{content: string(content), err: err}
}()
go func() {
content, err := io.ReadAll(started.Stderr())
stderrResult <- readResult{content: string(content), err: err}
}()
if _, err := io.WriteString(started.Stdin(), "owned\n"); err != nil {
t.Fatalf("write proof-owned stdin: %v", err)
}
if err := started.Stdin().Close(); err != nil {
t.Fatalf("close proof-owned stdin: %v", err)
}
if err := started.Child().Wait(); err != nil {
t.Fatalf("wait confined child: %v", err)
}
stdout := <-stdoutResult
stderr := <-stderrResult
if stdout.err != nil || stdout.content != "stdout:owned\n" {
t.Fatalf("stdout = %q, %v", stdout.content, stdout.err)
}
if stderr.err != nil || stderr.content != "stderr:owned\n" {
t.Fatalf("stderr = %q, %v", stderr.content, stderr.err)
}
if err := started.Abort(); err != nil {
t.Fatalf("cleanup completed handle: %v", err)
}
}
func TestConfinementStartCleansPipeAndProcessFailures(t *testing.T) {
t.Run("pipe setup", func(t *testing.T) {
var opened []*os.File
calls := 0
factory := func() (*os.File, *os.File, error) {
calls++
if calls == 3 {
return nil, nil, errors.New("injected pipe failure")
}
reader, writer, err := os.Pipe()
if err == nil {
opened = append(opened, reader, writer)
}
return reader, writer, err
}
if _, err := startConfinementCommandWithPipes(
exec.Command("true"),
factory,
); err == nil || !strings.Contains(err.Error(), "stderr pipe") {
t.Fatalf("pipe setup error = %v", err)
}
assertClosedFiles(t, opened)
})
t.Run("process start", func(t *testing.T) {
var opened []*os.File
factory := func() (*os.File, *os.File, error) {
reader, writer, err := os.Pipe()
if err == nil {
opened = append(opened, reader, writer)
}
return reader, writer, err
}
missing := filepath.Join(t.TempDir(), "missing-command")
if _, err := startConfinementCommandWithPipes(
exec.Command(missing),
factory,
); err == nil || !strings.Contains(err.Error(), "start confined child") {
t.Fatalf("process start error = %v", err)
}
assertClosedFiles(t, opened)
})
}
func TestConfinementAbortClosesPipesAndReapsChild(t *testing.T) {
started, err := startConfinementCommand(exec.Command("sleep", "5"))
if err != nil {
t.Fatalf("startConfinementCommand: %v", err)
}
stdin := started.Stdin()
stdout := started.Stdout()
stderr := started.Stderr()
if err := started.Abort(); err != nil {
t.Fatalf("Abort: %v", err)
}
if started.Child().ProcessState == nil {
t.Fatal("Abort did not reap the child")
}
if _, err := stdin.Write([]byte("leak")); err == nil {
t.Fatal("Abort left stdin open")
}
if _, err := stdout.Read(make([]byte, 1)); err == nil {
t.Fatal("Abort left stdout open")
}
if _, err := stderr.Read(make([]byte, 1)); err == nil {
t.Fatal("Abort left stderr open")
}
if err := started.Abort(); err != nil {
t.Fatalf("second Abort: %v", err)
}
}
func assertClosedFiles(t *testing.T, files []*os.File) {
t.Helper()
for _, file := range files {
if _, err := file.Stat(); err == nil {
t.Fatalf("pipe endpoint %d remained open", file.Fd())
}
}
}