feat(governance): add local bounded verification executor #230
@@ -0,0 +1,197 @@
|
||||
// Package developmentexecutor contains the local, read-only verification
|
||||
// adapter for governed development candidates. It is deliberately not a
|
||||
// source mutator, promotion authority, secret resolver, or deployment tool.
|
||||
package developmentexecutor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CheckKind string
|
||||
|
||||
const (
|
||||
CheckGoTest CheckKind = "GO_TEST"
|
||||
CheckGoVet CheckKind = "GO_VET"
|
||||
CheckGitDiffCheck CheckKind = "GIT_DIFF_CHECK"
|
||||
)
|
||||
|
||||
type Check struct {
|
||||
Kind CheckKind
|
||||
Packages []string
|
||||
}
|
||||
|
||||
type Plan struct {
|
||||
Worktree string
|
||||
WorktreeID string
|
||||
BaseRepoRevision string
|
||||
ProtectedMainAccess string
|
||||
ProductionAccess string
|
||||
SecretAccess string
|
||||
Checks []Check
|
||||
}
|
||||
|
||||
type CheckResult struct {
|
||||
Kind CheckKind `json:"kind"`
|
||||
Passed bool `json:"passed"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
DurationMillis int64 `json:"duration_millis"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
WorktreeID string `json:"worktree_id"`
|
||||
BaseRepoRevision string `json:"base_repo_revision"`
|
||||
Passed bool `json:"passed"`
|
||||
Checks []CheckResult `json:"checks"`
|
||||
EvidenceRef string `json:"evidence_ref"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidPlan = errors.New("invalid governed verification plan")
|
||||
ErrUnsupportedCheck = errors.New("unsupported governed verification check")
|
||||
ErrProtectedWorktree = errors.New("protected worktree is not executable")
|
||||
)
|
||||
|
||||
type CommandRunner interface {
|
||||
Run(context.Context, string, string, []string) int
|
||||
}
|
||||
|
||||
type OSCommandRunner struct{}
|
||||
|
||||
func (OSCommandRunner) Run(ctx context.Context, dir, name string, args []string) int {
|
||||
command := exec.CommandContext(ctx, name, args...)
|
||||
command.Dir = dir
|
||||
command.Stdin = nil
|
||||
command.Stdout = io.Discard
|
||||
command.Stderr = io.Discard
|
||||
command.Env = safeEnvironment()
|
||||
if err := command.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func ValidatePlan(plan Plan) error {
|
||||
if strings.TrimSpace(plan.Worktree) == "" || strings.TrimSpace(plan.WorktreeID) == "" || strings.TrimSpace(plan.BaseRepoRevision) == "" || len(plan.Checks) == 0 {
|
||||
return ErrInvalidPlan
|
||||
}
|
||||
if !filepath.IsAbs(plan.Worktree) {
|
||||
return ErrInvalidPlan
|
||||
}
|
||||
if plan.ProtectedMainAccess != "DENIED" || plan.ProductionAccess != "DENIED" || plan.SecretAccess != "DENIED" {
|
||||
return ErrProtectedWorktree
|
||||
}
|
||||
switch strings.ToLower(filepath.Base(filepath.Clean(plan.WorktreeID))) {
|
||||
case "main", "protected-main", "production":
|
||||
return ErrProtectedWorktree
|
||||
}
|
||||
for _, check := range plan.Checks {
|
||||
if err := validateCheck(check); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCheck(check Check) error {
|
||||
switch check.Kind {
|
||||
case CheckGoTest, CheckGoVet:
|
||||
if len(check.Packages) == 0 {
|
||||
return ErrInvalidPlan
|
||||
}
|
||||
for _, packagePath := range check.Packages {
|
||||
if strings.TrimSpace(packagePath) == "" || strings.ContainsAny(packagePath, ";&|`$()<>\r\n") || unsafePackagePath(packagePath) {
|
||||
return ErrInvalidPlan
|
||||
}
|
||||
}
|
||||
case CheckGitDiffCheck:
|
||||
if len(check.Packages) != 0 {
|
||||
return ErrInvalidPlan
|
||||
}
|
||||
default:
|
||||
return ErrUnsupportedCheck
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unsafePackagePath(value string) bool {
|
||||
value = strings.ReplaceAll(value, "\\", "/")
|
||||
for _, part := range strings.Split(value, "/") {
|
||||
if part == ".." {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return strings.HasPrefix(value, "../") || strings.HasPrefix(value, "/") || (len(value) > 1 && value[1] == ':')
|
||||
}
|
||||
|
||||
func Execute(ctx context.Context, plan Plan, runner CommandRunner) (Result, error) {
|
||||
if err := ValidatePlan(plan); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if runner == nil {
|
||||
runner = OSCommandRunner{}
|
||||
}
|
||||
if _, err := os.Stat(plan.Worktree); err != nil {
|
||||
return Result{}, fmt.Errorf("worktree unavailable: %w", err)
|
||||
}
|
||||
result := Result{WorktreeID: plan.WorktreeID, BaseRepoRevision: plan.BaseRepoRevision, Passed: true, Checks: make([]CheckResult, 0, len(plan.Checks))}
|
||||
for _, check := range plan.Checks {
|
||||
started := time.Now()
|
||||
name, args := commandFor(check)
|
||||
exitCode := runner.Run(ctx, plan.Worktree, name, args)
|
||||
passed := exitCode == 0
|
||||
result.Checks = append(result.Checks, CheckResult{Kind: check.Kind, Passed: passed, ExitCode: exitCode, DurationMillis: time.Since(started).Milliseconds()})
|
||||
if !passed {
|
||||
result.Passed = false
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
result.Passed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
result.EvidenceRef = evidenceRef(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func commandFor(check Check) (string, []string) {
|
||||
switch check.Kind {
|
||||
case CheckGoTest:
|
||||
return "go", append([]string{"test"}, check.Packages...)
|
||||
case CheckGoVet:
|
||||
return "go", append([]string{"vet"}, check.Packages...)
|
||||
default:
|
||||
return "git", []string{"diff", "--check"}
|
||||
}
|
||||
}
|
||||
|
||||
func evidenceRef(result Result) string {
|
||||
hash := sha256.New()
|
||||
_, _ = fmt.Fprintf(hash, "%s|%s|%t|", result.WorktreeID, result.BaseRepoRevision, result.Passed)
|
||||
for _, check := range result.Checks {
|
||||
_, _ = fmt.Fprintf(hash, "%s|%t|%d|", check.Kind, check.Passed, check.ExitCode)
|
||||
}
|
||||
return "verification:" + hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func safeEnvironment() []string {
|
||||
allowed := []string{"PATH", "SystemRoot", "TEMP", "TMP", "GOROOT", "GOPATH", "GOMODCACHE"}
|
||||
result := make([]string, 0, len(allowed))
|
||||
for _, name := range allowed {
|
||||
if value, ok := os.LookupEnv(name); ok && value != "" {
|
||||
result = append(result, name+"="+value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package developmentexecutor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
calls []string
|
||||
exits []int
|
||||
}
|
||||
|
||||
func (r *fakeRunner) Run(_ context.Context, _ string, name string, args []string) int {
|
||||
call := name
|
||||
for _, arg := range args {
|
||||
call += " " + arg
|
||||
}
|
||||
r.calls = append(r.calls, call)
|
||||
if len(r.exits) == 0 {
|
||||
return 0
|
||||
}
|
||||
exit := r.exits[0]
|
||||
r.exits = r.exits[1:]
|
||||
return exit
|
||||
}
|
||||
|
||||
func validPlan(t *testing.T) Plan {
|
||||
t.Helper()
|
||||
worktree, err := filepath.Abs(".")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Plan{Worktree: worktree, WorktreeID: "sandbox-1", BaseRepoRevision: "revision-1", ProtectedMainAccess: "DENIED", ProductionAccess: "DENIED", SecretAccess: "DENIED", Checks: []Check{{Kind: CheckGoTest, Packages: []string{"./modules/work/..."}}, {Kind: CheckGoVet, Packages: []string{"./modules/work/..."}}, {Kind: CheckGitDiffCheck}}}
|
||||
}
|
||||
|
||||
func TestExecuteRunsOnlyTypedChecksAndReturnsEvidence(t *testing.T) {
|
||||
runner := &fakeRunner{}
|
||||
result, err := Execute(context.Background(), validPlan(t), runner)
|
||||
if err != nil || !result.Passed || len(result.Checks) != 3 || result.EvidenceRef == "" {
|
||||
t.Fatalf("result=%+v err=%v", result, err)
|
||||
}
|
||||
if len(runner.calls) != 3 || runner.calls[0] != "go test ./modules/work/..." || runner.calls[2] != "git diff --check" {
|
||||
t.Fatalf("calls=%v", runner.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteFailureCannotBecomeVerificationSuccess(t *testing.T) {
|
||||
runner := &fakeRunner{exits: []int{1, 0}}
|
||||
plan := validPlan(t)
|
||||
plan.Checks = []Check{{Kind: CheckGoTest, Packages: []string{"./..."}}, {Kind: CheckGitDiffCheck}}
|
||||
result, err := Execute(context.Background(), plan, runner)
|
||||
if err != nil || result.Passed || result.Checks[0].Passed || result.EvidenceRef == "" {
|
||||
t.Fatalf("failed result was overclaimed: %+v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePlanRejectsProtectedAndShellLikeInputs(t *testing.T) {
|
||||
protected := validPlan(t)
|
||||
protected.WorktreeID = "production"
|
||||
if !errors.Is(ValidatePlan(protected), ErrProtectedWorktree) {
|
||||
t.Fatal("protected worktree accepted")
|
||||
}
|
||||
injection := validPlan(t)
|
||||
injection.Checks = []Check{{Kind: CheckGoTest, Packages: []string{"./...; echo leaked"}}}
|
||||
if !errors.Is(ValidatePlan(injection), ErrInvalidPlan) {
|
||||
t.Fatal("shell-like package accepted")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000251-reflection-cognition-routing.json",
|
||||
"task_id": "DEV-000251",
|
||||
"task_file": "dev/tasks/DEV-000252-local-governed-verification-executor.json",
|
||||
"task_id": "DEV-000252",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000252",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "c925089fd2304213758d2721a0592ca73d765047",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add a local governed verification executor that runs only typed, bounded checks inside an isolated worktree and returns evidence without source, secret, production or promotion authority.",
|
||||
"requirement_ids": ["governed self-development", "isolated verification", "independent test evidence"],
|
||||
"allowed_change_scope": {"modules": ["governed-development"], "paths": ["adapters/developmentexecutor/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000252-local-governed-verification-executor.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "LLM-local/**", "modules/work/**", "cmd/core-runtime/**", "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 DevelopmentScaffold semantics", "typed local check plan"], "may_change": []},
|
||||
"semantic_owner": "governed development verification adapter",
|
||||
"canonical_state": ["none; executor returns evidence to a future gate owner"],
|
||||
"state_transitions": ["typed plan -> validated sandbox checks -> immutable result summary"],
|
||||
"side_effects_and_privileges": ["read-only source/test execution in supplied isolated worktree; no secrets, production, protected main or promotion access"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["each check has an explicit result; failed or cancelled checks never become verification success; rerun creates a new result"],
|
||||
"assumptions": ["worktree isolation and gate persistence are supplied by the surrounding governed control plane"],
|
||||
"acceptance_scenarios": ["allowed go test/vet and git diff check plans validate", "shell injection and unsupported commands reject", "protected or production worktree rejects", "failed check returns non-success evidence", "result contains no command output or secret material"],
|
||||
"required_tests": ["go test ./adapters/developmentexecutor", "go vet ./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"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
Reference in New Issue
Block a user