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:
2026-08-28 15:27:39 +04:00
parent 98f1b2dccc
commit fb7135e1d9
10 changed files with 87 additions and 27 deletions
+42 -4
View File
@@ -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")
}
}