독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
236 lines
7.4 KiB
Go
236 lines
7.4 KiB
Go
package agenttask
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestValidatePreparedIsolationRequiresExactConfinementProof(t *testing.T) {
|
|
projectID := ProjectID("project")
|
|
workspaceID := WorkspaceID("workspace")
|
|
project := ProjectRecord{
|
|
ProjectID: projectID,
|
|
WorkspaceID: workspaceID,
|
|
Intent: &StartIntent{
|
|
ProjectID: projectID,
|
|
WorkspaceID: workspaceID,
|
|
ConfigRevision: "config-r1",
|
|
GrantRevision: "grant-r1",
|
|
WorkflowRevision: "workflow-r1",
|
|
},
|
|
}
|
|
work := WorkRecord{
|
|
Unit: testUnit("work", WriteSetUnknown),
|
|
AttemptID: "work#1",
|
|
}
|
|
target := ExecutionTarget{
|
|
ProviderID: "provider",
|
|
ModelID: "model",
|
|
ProfileID: "profile",
|
|
ProfileRevision: "profile-r1",
|
|
ConfigRevision: "config-r1",
|
|
Capacity: 1,
|
|
}
|
|
isolation := newFakeIsolation(t)
|
|
prepared, err := isolation.Prepare(context.Background(), IsolationRequest{
|
|
Project: project,
|
|
Work: work,
|
|
Target: target,
|
|
IdempotencyKey: "dispatch/project/work/1/isolation",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Prepare: %v", err)
|
|
}
|
|
if _, _, err := validatePreparedIsolation(project, work, target, prepared); err != nil {
|
|
t.Fatalf("validate exact proof: %v", err)
|
|
}
|
|
|
|
missing := prepared
|
|
missing.Confinement = nil
|
|
if _, _, err := validatePreparedIsolation(project, work, target, missing); err == nil ||
|
|
!strings.Contains(err.Error(), "incomplete strict ports") {
|
|
t.Fatalf("missing proof error = %v", err)
|
|
}
|
|
|
|
tampered := prepared
|
|
tamperedProof := *prepared.Confinement.(*fakeConfinement)
|
|
tamperedProof.binding = prepared.Confinement.Binding()
|
|
tamperedProof.binding.ProfileRevision = "profile-r2"
|
|
tampered.Confinement = &tamperedProof
|
|
if _, _, err := validatePreparedIsolation(project, work, target, tampered); err == nil ||
|
|
!strings.Contains(err.Error(), "invalid executable confinement proof") {
|
|
t.Fatalf("tampered proof error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestManagerOwnsConfinementStartBeforeBindingProviderInvocation(t *testing.T) {
|
|
commandType := reflect.TypeOf(ConfinementCommand{})
|
|
for _, forbidden := range []string{"Stdin", "Stdout", "Stderr"} {
|
|
if _, exists := commandType.FieldByName(forbidden); exists {
|
|
t.Fatalf("ConfinementCommand still exposes caller-owned %s", forbidden)
|
|
}
|
|
}
|
|
snapshot := testSnapshot("project", "workspace", testUnit("work", WriteSetUnknown))
|
|
harness := newHarness(t, map[ProjectID]ProjectWorkflowSnapshot{"project": snapshot}, 1)
|
|
harness.start("project", "workspace", nil)
|
|
if err := harness.manager.Reconcile(context.Background()); err != nil {
|
|
t.Fatalf("Reconcile: %v", err)
|
|
}
|
|
proof := harness.isolation.proof(dispatchKey("project", "work", 1) + "/isolation")
|
|
if proof == nil {
|
|
t.Fatal("isolation did not retain the executable proof")
|
|
}
|
|
if proof.startCount() != 1 {
|
|
t.Fatalf("confinement starts = %d, want 1", proof.startCount())
|
|
}
|
|
commands := proof.startCommands()
|
|
if len(commands) != 1 || commands[0].Name != "true" {
|
|
t.Fatalf("confinement command = %#v, want exact prepared command", commands)
|
|
}
|
|
prepared, bound, proofStartsAtBind := harness.invoker.launchStats()
|
|
if prepared != 1 || bound != 1 || len(proofStartsAtBind) != 1 || proofStartsAtBind[0] != 1 {
|
|
t.Fatalf(
|
|
"launch ordering prepare=%d bind=%d proof-starts-at-bind=%v, want 1/1/[1]",
|
|
prepared,
|
|
bound,
|
|
proofStartsAtBind,
|
|
)
|
|
}
|
|
proofHandles := proof.startedHandles()
|
|
boundHandles := harness.invoker.startedHandles()
|
|
if len(proofHandles) != 1 || len(boundHandles) != 1 ||
|
|
proofHandles[0] != boundHandles[0] {
|
|
t.Fatalf(
|
|
"proof/bind handles = %#v/%#v, want one exact shared handle",
|
|
proofHandles,
|
|
boundHandles,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestManagerConfinementLaunchFailuresDoNotBindOrLeakChildren(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
configure func(*managerHarness)
|
|
wantStarts int
|
|
wantPrepare int
|
|
wantBind int
|
|
wantChild bool
|
|
wantHandle bool
|
|
wantAbort int
|
|
}{
|
|
{
|
|
name: "prepare failure",
|
|
configure: func(harness *managerHarness) {
|
|
harness.invoker.prepareErr = errors.New("prepare failed")
|
|
},
|
|
wantPrepare: 1,
|
|
},
|
|
{
|
|
name: "proof start failure",
|
|
configure: func(harness *managerHarness) {
|
|
harness.isolation.startErr = errors.New("proof start failed")
|
|
},
|
|
wantStarts: 1,
|
|
wantPrepare: 1,
|
|
},
|
|
{
|
|
name: "nil started handle",
|
|
configure: func(harness *managerHarness) {
|
|
harness.isolation.nilStarted = true
|
|
},
|
|
wantStarts: 1,
|
|
wantPrepare: 1,
|
|
},
|
|
{
|
|
name: "invalid started handle aborts child and pipes",
|
|
configure: func(harness *managerHarness) {
|
|
harness.isolation.invalidStart = true
|
|
harness.invoker.command = ConfinementCommand{Name: "sleep", Args: []string{"5"}}
|
|
},
|
|
wantStarts: 1,
|
|
wantPrepare: 1,
|
|
wantChild: true,
|
|
wantHandle: true,
|
|
wantAbort: 1,
|
|
},
|
|
{
|
|
name: "bind failure reaps child",
|
|
configure: func(harness *managerHarness) {
|
|
harness.invoker.bindErr = errors.New("bind failed")
|
|
harness.invoker.command = ConfinementCommand{Name: "sleep", Args: []string{"5"}}
|
|
},
|
|
wantStarts: 1,
|
|
wantPrepare: 1,
|
|
wantBind: 1,
|
|
wantChild: true,
|
|
wantHandle: true,
|
|
wantAbort: 1,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
snapshot := testSnapshot("project", "workspace", testUnit("work", WriteSetUnknown))
|
|
harness := newHarness(t, map[ProjectID]ProjectWorkflowSnapshot{"project": snapshot}, 1)
|
|
test.configure(harness)
|
|
harness.start("project", "workspace", nil)
|
|
if err := harness.manager.Reconcile(context.Background()); err != nil {
|
|
t.Fatalf("Reconcile: %v", err)
|
|
}
|
|
proof := harness.isolation.proof(dispatchKey("project", "work", 1) + "/isolation")
|
|
if proof == nil {
|
|
t.Fatal("isolation did not retain the executable proof")
|
|
}
|
|
prepared, bound, _ := harness.invoker.launchStats()
|
|
if proof.startCount() != test.wantStarts || prepared != test.wantPrepare || bound != test.wantBind {
|
|
t.Fatalf(
|
|
"starts/prepare/bind = %d/%d/%d, want %d/%d/%d",
|
|
proof.startCount(),
|
|
prepared,
|
|
bound,
|
|
test.wantStarts,
|
|
test.wantPrepare,
|
|
test.wantBind,
|
|
)
|
|
}
|
|
children := proof.startedChildren()
|
|
if test.wantChild {
|
|
if len(children) != 1 || children[0].ProcessState == nil {
|
|
t.Fatalf("bind failure leaked confined child: %#v", children)
|
|
}
|
|
} else if len(children) != 0 {
|
|
t.Fatalf("failed launch unexpectedly started children: %#v", children)
|
|
}
|
|
handles := proof.startedHandles()
|
|
if test.wantHandle {
|
|
if len(handles) != 1 {
|
|
t.Fatalf("started handles = %#v, want one", handles)
|
|
}
|
|
started, ok := handles[0].(*fakeStartedConfinement)
|
|
if !ok {
|
|
t.Fatalf("started handle type = %T", handles[0])
|
|
}
|
|
if started.abortCount() != test.wantAbort {
|
|
t.Fatalf("handle aborts = %d, want %d", started.abortCount(), test.wantAbort)
|
|
}
|
|
if _, err := started.stdin.Write([]byte("leak")); err == nil {
|
|
t.Fatal("partial-start cleanup left stdin open")
|
|
}
|
|
if _, err := started.stdout.Read(make([]byte, 1)); err == nil {
|
|
t.Fatal("partial-start cleanup left stdout open")
|
|
}
|
|
if _, err := started.stderr.Read(make([]byte, 1)); err == nil {
|
|
t.Fatal("partial-start cleanup left stderr open")
|
|
}
|
|
} else if len(handles) != 0 {
|
|
t.Fatalf("failed launch unexpectedly returned handles: %#v", handles)
|
|
}
|
|
if harness.invoker.callCount() != 0 {
|
|
t.Fatalf("failed launch submitted %d provider invocations", harness.invoker.callCount())
|
|
}
|
|
})
|
|
}
|
|
}
|