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
+4 -4
View File
@@ -26,7 +26,7 @@ func request(q string) domain.DecisionRequest {
func TestDecisionRequestBlocksAndResumes(t *testing.T) {
s, id := phaseStore(t)
lease(t, s, id)
project := registry.Project{ID: "p"}
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}}
if _, err := RequestHumanDecision(s, project, id, request("should the old cache contract stay compatible?")); err != nil {
t.Fatal(err)
@@ -78,7 +78,7 @@ func TestDecisionRequestBlocksAndResumes(t *testing.T) {
func TestResolvedQuestionIsNotRepeatedAfterRotation(t *testing.T) {
s, id := phaseStore(t)
lease(t, s, id)
project := registry.Project{ID: "p"}
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}}
if _, err := RequestHumanDecision(s, project, id, request("preserve compatibility?")); err != nil {
t.Fatal(err)
}
@@ -100,7 +100,7 @@ func TestResolvedQuestionIsNotRepeatedAfterRotation(t *testing.T) {
// The budget stops a task turning into an interview.
func TestDecisionBudgetBecomesOperatorRequired(t *testing.T) {
s, id := phaseStore(t)
project := registry.Project{ID: "p"}
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}}
project.HumanDecisions.MaxRequestsPerTask = 2
for i, q := range []string{"first?", "second?"} {
@@ -134,7 +134,7 @@ func TestDecisionBudgetBecomesOperatorRequired(t *testing.T) {
// Bounds are the whole defence against an interview arriving as one request.
func TestDecisionRequestBoundsRejectInterviews(t *testing.T) {
s, id := phaseStore(t)
project := registry.Project{ID: "p"}
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}}
cases := map[string]domain.DecisionRequest{
"no question": {Why: "w"},
"no why": {Question: "q"},