Refuse a plan command outside project policy when the plan seals
The brief tells the planner "a command outside its policy is refused when you seal, not later". It was not. The only caller of VerificationPolicy.Allows was PlanPhaseCommands, which runs when the implementer asks to verify: one phase, one session and one rotation after the planner could have fixed it. Run 9 sealed ["bash", "scripts/test_healthcheck.sh"] against a policy that allows neither shape, and the phase request was accepted. The check now runs beside citation resolution, on the coordinator, where the project is already in scope. A project with no verification policy can still seal a plan; it cannot seal one that declares run: lines, which matches what an absent policy already meant at verification time. Test fixtures gained a policy for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
@@ -3,6 +3,7 @@ package operations
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -47,6 +48,7 @@ func sealed(t *testing.T, v interface{ Validate() error }) []byte {
|
||||
}
|
||||
|
||||
var research = workphase.Research{Findings: []workphase.Finding{{ID: "r1", Confidence: workphase.Fact, Claim: "runs per figure", Evidence: "attr.go:88"}}}
|
||||
|
||||
// planDoc is a real sealed specification. It cites research:r1, which the
|
||||
// research fixture above contains, so the reference check has something to
|
||||
// resolve.
|
||||
@@ -94,7 +96,7 @@ None.
|
||||
|
||||
func TestFullPhasePathSealsEachArtifact(t *testing.T) {
|
||||
s, id := phaseStore(t)
|
||||
project := registry.Project{ID: "p"}
|
||||
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}}
|
||||
|
||||
// frame -> research needs no artifact: framing produces none.
|
||||
if _, err := AdvanceWorkPhase(s, project, id, nil); err != nil {
|
||||
@@ -154,7 +156,7 @@ func TestFullPhasePathSealsEachArtifact(t *testing.T) {
|
||||
// A project that declares a short path skips the phases it omits.
|
||||
func TestProjectPathSkipsUndeclaredPhases(t *testing.T) {
|
||||
s, id := phaseStore(t)
|
||||
project := registry.Project{ID: "p", WorkPhases: []domain.WorkPhase{domain.WorkPhaseFrame, domain.WorkPhaseImplement, domain.WorkPhaseReview}}
|
||||
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}, WorkPhases: []domain.WorkPhase{domain.WorkPhaseFrame, domain.WorkPhaseImplement, domain.WorkPhaseReview}}
|
||||
if _, err := AdvanceWorkPhase(s, project, id, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -180,7 +182,7 @@ func TestIllegalTransitionRejectedAtTheAppendBoundary(t *testing.T) {
|
||||
|
||||
func TestEndOfPathIsRefused(t *testing.T) {
|
||||
s, id := phaseStore(t)
|
||||
project := registry.Project{ID: "p", WorkPhases: []domain.WorkPhase{domain.WorkPhaseFrame}}
|
||||
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}, WorkPhases: []domain.WorkPhase{domain.WorkPhaseFrame}}
|
||||
if _, err := AdvanceWorkPhase(s, project, id, nil); !errors.Is(err, domain.ErrInvalid) {
|
||||
t.Fatalf("want ErrInvalid at the end of the path, got %v", err)
|
||||
}
|
||||
@@ -192,7 +194,7 @@ func TestPhaseChangeDoesNotTouchLifecycle(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before, _ := s.Task(id)
|
||||
if _, err := AdvanceWorkPhase(s, registry.Project{ID: "p"}, id, nil); err != nil {
|
||||
if _, err := AdvanceWorkPhase(s, registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}}, id, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after, _ := s.Task(id)
|
||||
@@ -206,3 +208,39 @@ func TestPhaseChangeDoesNotTouchLifecycle(t *testing.T) {
|
||||
t.Fatalf("phase = %q", after.WorkPhase)
|
||||
}
|
||||
}
|
||||
|
||||
// The brief promises the planner that a command outside the project's policy
|
||||
// is refused when the plan seals. Run 9 sealed
|
||||
// ["bash", "scripts/test_healthcheck.sh"] against a policy allowing neither
|
||||
// shape: the only check lived in PlanPhaseCommands, one phase too late.
|
||||
func TestPlanSealRefusesACommandOutsideProjectPolicy(t *testing.T) {
|
||||
s, id := phaseStore(t)
|
||||
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "vet", "./..."}}}}
|
||||
if _, err := AdvanceWorkPhase(s, project, id, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := AdvanceWorkPhase(s, project, id, sealed(t, research)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := AdvanceWorkPhase(s, project, id, planDoc)
|
||||
if !errors.Is(err, domain.ErrInvalid) {
|
||||
t.Fatalf("a plan command outside policy must be refused at seal, got %v", err)
|
||||
}
|
||||
for _, want := range []string{"phase-1", "go test"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("refusal never names %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
if got, _ := s.Task(id); got.PlanRef != "" || got.WorkPhase != domain.WorkPhasePlan {
|
||||
t.Fatalf("a refused plan was sealed anyway: %+v", got)
|
||||
}
|
||||
|
||||
// The same plan seals once the project allows the command.
|
||||
project.Verification.Allowed = append(project.Verification.Allowed, []string{"go", "test", "*"})
|
||||
if _, err := AdvanceWorkPhase(s, project, id, planDoc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := s.Task(id); got.PlanRef == "" {
|
||||
t.Fatal("an allowed plan did not seal")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user