diff --git a/cmd/core-runtime/development_gate.go b/cmd/core-runtime/development_gate.go index 1d28a82..17c0e55 100644 --- a/cmd/core-runtime/development_gate.go +++ b/cmd/core-runtime/development_gate.go @@ -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 { diff --git a/cmd/core-runtime/development_gate_test.go b/cmd/core-runtime/development_gate_test.go index 68bdd43..548fd84 100644 --- a/cmd/core-runtime/development_gate_test.go +++ b/cmd/core-runtime/development_gate_test.go @@ -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) diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index a293e3b..3bdad98 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -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 { diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index dc3c3a0..163498c 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -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 } diff --git a/dev/tasks/DEV-000253-development-gate-verifier-integration.json b/dev/tasks/DEV-000253-development-gate-verifier-integration.json new file mode 100644 index 0000000..f56a8f4 --- /dev/null +++ b/dev/tasks/DEV-000253-development-gate-verifier-integration.json @@ -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"] +}