feat(governance): connect verifier to DevelopmentGate #231

Merged
m merged 1 commits from codex/dev-000248-self-development-runtime-status into main 2026-08-23 18:32:28 +00:00
5 changed files with 217 additions and 2 deletions
+56
View File
@@ -1,14 +1,70 @@
package main
import (
"context"
"errors"
"time"
"git.cin.su/m/Assistant/adapters/developmentexecutor"
"git.cin.su/m/Assistant/contracts/work"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/modules/work/ports"
)
var errDevelopmentGateCreate = errors.New("development gate must start at evidence collected with revision one")
var errDevelopmentGateNotIsolated = errors.New("development gate is not isolated")
var errDevelopmentVerifierNotIndependent = errors.New("development verifier is not independent")
var errDevelopmentWorktreeMismatch = errors.New("verification worktree does not match gate")
var errDevelopmentChecksFailed = errors.New("development verification checks failed")
type developmentGateVerificationRequest struct {
GateID string
ExpectedRevision int64
VerifierRunID work.AgentRunID
Plan developmentexecutor.Plan
}
func verifyDevelopmentGate(ctx context.Context, store ports.DevelopmentGateStore, request developmentGateVerificationRequest, now time.Time, runner developmentexecutor.CommandRunner) (work.DevelopmentGate, developmentexecutor.Result, error) {
gate, err := store.LoadDevelopmentGate(ctx, request.GateID)
if err != nil {
return work.DevelopmentGate{}, developmentexecutor.Result{}, err
}
if request.ExpectedRevision < 1 || gate.Revision != request.ExpectedRevision || gate.Stage != work.DevelopmentIsolated {
return gate, developmentexecutor.Result{}, errDevelopmentGateNotIsolated
}
if request.VerifierRunID == "" || sameRun(request.VerifierRunID, gate.ProducerRunRefs) {
return gate, developmentexecutor.Result{}, errDevelopmentVerifierNotIndependent
}
if request.Plan.WorktreeID != gate.WorktreeRef {
return gate, developmentexecutor.Result{}, errDevelopmentWorktreeMismatch
}
result, err := developmentexecutor.Execute(ctx, request.Plan, runner)
if err != nil {
return gate, result, err
}
if !result.Passed {
return gate, result, errDevelopmentChecksFailed
}
gate.VerifierRunRefs = append(append([]work.AgentRunID(nil), gate.VerifierRunRefs...), request.VerifierRunID)
gate.VerificationEvidence = result.EvidenceRef
advanced, err := advanceDevelopmentGate(gate, work.DevelopmentVerified, now)
if err != nil {
return gate, result, err
}
if err := store.SaveDevelopmentGate(ctx, advanced, request.ExpectedRevision); err != nil {
return gate, result, err
}
return advanced, result, nil
}
func sameRun(value work.AgentRunID, runs []work.AgentRunID) bool {
for _, run := range runs {
if run == value {
return true
}
}
return false
}
func prepareDevelopmentGate(value work.DevelopmentGate, now time.Time) (work.DevelopmentGate, error) {
if value.Stage != work.DevelopmentEvidenceCollected || value.Revision != 1 {
+112
View File
@@ -1,12 +1,124 @@
package main
import (
"context"
"path/filepath"
"testing"
"time"
"git.cin.su/m/Assistant/adapters/developmentexecutor"
"git.cin.su/m/Assistant/contracts/work"
)
type developmentGateStoreStub struct {
gate work.DevelopmentGate
saved bool
expected int64
}
func (s *developmentGateStoreStub) LoadDevelopmentGate(context.Context, string) (work.DevelopmentGate, error) {
return s.gate, nil
}
func (s *developmentGateStoreStub) SaveDevelopmentGate(_ context.Context, gate work.DevelopmentGate, expected int64) error {
s.gate = gate
s.saved = true
s.expected = expected
return nil
}
type developmentCommandRunnerStub struct {
exitCodes []int
commands int
}
func (r *developmentCommandRunnerStub) Run(context.Context, string, string, []string) int {
code := 0
if r.commands < len(r.exitCodes) {
code = r.exitCodes[r.commands]
}
r.commands++
return code
}
func validDevelopmentVerificationPlan(t *testing.T, worktreeID string) developmentexecutor.Plan {
t.Helper()
worktree, err := filepath.Abs(".")
if err != nil {
t.Fatal(err)
}
return developmentexecutor.Plan{
Worktree: worktree,
WorktreeID: worktreeID,
BaseRepoRevision: "base-revision",
ProtectedMainAccess: "DENIED",
ProductionAccess: "DENIED",
SecretAccess: "DENIED",
Checks: []developmentexecutor.Check{{Kind: developmentexecutor.CheckGitDiffCheck}},
}
}
func isolatedDevelopmentGate() work.DevelopmentGate {
return work.DevelopmentGate{
ID: "gate-1",
Revision: 4,
Stage: work.DevelopmentIsolated,
EvidenceRefs: []string{"evidence-1"},
ProposalRef: "proposal-1",
TaskPacketRef: "task-1",
WorktreeRef: "sandbox-1",
ProducerRunRefs: []work.AgentRunID{"producer-1"},
VerifierRunRefs: nil,
VerificationEvidence: "",
}
}
func TestVerifyDevelopmentGatePersistsVerifiedWithIndependentEvidence(t *testing.T) {
store := &developmentGateStoreStub{gate: isolatedDevelopmentGate()}
runner := &developmentCommandRunnerStub{exitCodes: []int{0}}
gate, result, err := verifyDevelopmentGate(context.Background(), store, developmentGateVerificationRequest{
GateID: "gate-1",
ExpectedRevision: 4,
VerifierRunID: "verifier-1",
Plan: validDevelopmentVerificationPlan(t, "sandbox-1"),
}, time.UnixMilli(20), runner)
if err != nil {
t.Fatal(err)
}
if !result.Passed || gate.Stage != work.DevelopmentVerified || gate.Revision != 5 {
t.Fatalf("verification did not advance gate: gate=%#v result=%#v", gate, result)
}
if !store.saved || store.expected != 4 || gate.VerificationEvidence == "" {
t.Fatalf("verification was not durably persisted: store=%#v gate=%#v", store, gate)
}
}
func TestVerifyDevelopmentGateLeavesIsolatedOnFailedChecks(t *testing.T) {
store := &developmentGateStoreStub{gate: isolatedDevelopmentGate()}
gate, result, err := verifyDevelopmentGate(context.Background(), store, developmentGateVerificationRequest{
GateID: "gate-1",
ExpectedRevision: 4,
VerifierRunID: "verifier-1",
Plan: validDevelopmentVerificationPlan(t, "sandbox-1"),
}, time.UnixMilli(20), &developmentCommandRunnerStub{exitCodes: []int{1}})
if err != errDevelopmentChecksFailed || result.Passed || gate.Stage != work.DevelopmentIsolated || store.saved {
t.Fatalf("failed checks changed gate unexpectedly: gate=%#v result=%#v err=%v store=%#v", gate, result, err, store)
}
}
func TestVerifyDevelopmentGateRejectsProducerAsVerifier(t *testing.T) {
store := &developmentGateStoreStub{gate: isolatedDevelopmentGate()}
_, _, err := verifyDevelopmentGate(context.Background(), store, developmentGateVerificationRequest{
GateID: "gate-1",
ExpectedRevision: 4,
VerifierRunID: "producer-1",
Plan: validDevelopmentVerificationPlan(t, "sandbox-1"),
}, time.UnixMilli(20), &developmentCommandRunnerStub{exitCodes: []int{0}})
if err != errDevelopmentVerifierNotIndependent {
t.Fatalf("producer was accepted as verifier: %v", err)
}
}
func TestDevelopmentGateControllerRequiresSequentialVerifiedEvidence(t *testing.T) {
now := time.UnixMilli(10)
gate, err := prepareDevelopmentGate(work.DevelopmentGate{ID: "gate-1", Revision: 1, Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"evidence-1"}}, now)
+24
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"git.cin.su/m/Assistant/adapters/developmentexecutor"
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
@@ -311,6 +312,29 @@ func main() {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(gate)
})
mux.HandleFunc("POST /v1/creator/development/gates/{id}/verify", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request struct {
ExpectedRevision int64 `json:"expected_revision"`
VerifierRunID work.AgentRunID `json:"verifier_run_id"`
Plan developmentexecutor.Plan `json:"plan"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.ExpectedRevision < 1 || request.VerifierRunID == "" {
http.Error(w, "invalid development verification request", http.StatusBadRequest)
return
}
gate, result, err := verifyDevelopmentGate(r.Context(), workRepository, developmentGateVerificationRequest{GateID: r.PathValue("id"), ExpectedRevision: request.ExpectedRevision, VerifierRunID: request.VerifierRunID, Plan: request.Plan}, time.Now(), developmentexecutor.OSCommandRunner{})
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"gate": gate, "verification": result})
})
mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000252-local-governed-verification-executor.json",
"task_id": "DEV-000252",
"task_file": "dev/tasks/DEV-000253-development-gate-verifier-integration.json",
"task_id": "DEV-000253",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000253",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "f2e520e31e27563a17f66c73ff68b35bc93963a0",
"risk_tier": "E",
"intent": "Connect the local bounded verification executor to the authenticated durable DevelopmentGate ISOLATED-to-VERIFIED transition with optimistic revision and independent verifier checks.",
"requirement_ids": ["governed self-development", "independent verification", "durable gate progression"],
"allowed_change_scope": {"modules": ["governed-development"], "paths": ["cmd/core-runtime/development_gate.go", "cmd/core-runtime/main.go", "cmd/core-runtime/development_gate_test.go", "adapters/developmentexecutor/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000253-development-gate-verifier-integration.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "LLM-local/**", "modules/work/**", "cmd/agent-worker/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing authenticated Creator session", "existing DevelopmentGate CAS store", "local developmentexecutor"], "may_change": ["internal authenticated verifier route" ]},
"semantic_owner": "Work-owned DevelopmentGate controller",
"canonical_state": ["existing work.development_gates row only"],
"state_transitions": ["ISOLATED -> VERIFIED only after bounded checks pass and verifier run differs from producer runs"],
"side_effects_and_privileges": ["read-only checks in isolated worktree; one existing Work-owner CAS update; no source mutation, secrets, production or promotion authority"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["stale gate revision conflicts; failed checks leave gate ISOLATED; rerun uses a new verifier run/evidence reference"],
"assumptions": ["worktree has already been created by the governed isolation step"],
"acceptance_scenarios": ["unauthenticated verifier request denied", "non-ISOLATED gate denied", "same producer/verifier run denied", "failed check leaves gate unchanged", "passing checks persist VERIFIED with evidence and CAS"],
"required_tests": ["go test ./cmd/core-runtime ./adapters/developmentexecutor", "go vet ./cmd/core-runtime ./adapters/developmentexecutor", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "git diff --check", "independent", "CI", "production health smoke", "authenticated gate verifier live smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}