Compare commits

..
Author SHA1 Message Date
m c60a575fda feat: add isolated governed scaffold candidate
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 57s
2026-08-23 01:36:03 +03:00
m a09a7e811f Merge pull request 'feat: generate governed extension proposals from repeated evidence' (#159) from codex/wp-500-reflection-worker into main
CI / test (push) Successful in 1m10s
2026-08-22 22:29:49 +00:00
6 changed files with 155 additions and 2 deletions
+45
View File
@@ -82,3 +82,48 @@ type ExtensionProposal struct {
Assumptions []string
CreatedAtUnixMillis int64
}
type DevelopmentScaffoldStage string
const DevelopmentScaffoldSandboxed DevelopmentScaffoldStage = "SANDBOXED"
// DevelopmentScaffoldRequest describes the boundary a later control plane
// would use to create an isolated worktree. This value object itself performs
// no checkout or filesystem operation.
type DevelopmentScaffoldRequest struct {
ID string
ProposalRef string
BaseRepoRevision string
WorktreeRef string
TaskIntent string
Modules []string
AllowedPaths []string
ForbiddenPaths []string
ProtectedPaths []string
RequiredTests []string
ArtifactLineageRefs []string
ProtectedMainAccess string
ProductionAccess string
SecretAccess string
CreatedAt time.Time
}
// DevelopmentScaffold is a provenance-linked task-packet candidate. It has
// no executable instruction, credential, checkout handle or authority grant.
type DevelopmentScaffold struct {
ID string
Stage DevelopmentScaffoldStage
ProposalRef string
BaseRepoRevision string
WorktreeRef string
TaskIntent string
Modules []string
AllowedPaths []string
ForbiddenPaths []string
RequiredTests []string
ArtifactLineageRefs []string
ProtectedMainAccess string
ProductionAccess string
SecretAccess string
CreatedAtUnixMillis int64
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000186-extension-proposal-generator.json",
"task_id": "DEV-000186",
"task_file": "dev/tasks/DEV-000187-scaffold-task-packet-candidate.json",
"task_id": "DEV-000187",
"task_revision": 1
}
@@ -0,0 +1,26 @@
{
"task_id": "DEV-000187",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "a09a7e8",
"risk_tier": "E",
"intent": "Add a fail-closed typed scaffold candidate factory that produces an isolated task-packet descriptor without creating files, reading secrets, or accessing protected main/production.",
"requirement_ids": ["WP-602", "Scoped task packet candidate", "Isolated worktree provenance", "Protected path denial"],
"allowed_change_scope": {
"modules": ["work-agent-execution"],
"paths": ["contracts/work/self_development.go", "modules/work/domain/self_development.go", "modules/work/domain/self_development_test.go", "tests/semantic_evolution/self_development_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000187-scaffold-task-packet-candidate.json"]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "cmd/**", "adapters/**", "deploy/**", "contracts/authority/**", ".gitea/**"]},
"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 ExtensionProposal", "ADR-0022 governed development boundary"], "may_change": ["typed Work scaffold candidate value objects"]},
"semantic_owner": "work-agent-execution",
"canonical_state": ["No durable state; isolated scaffold candidate value object only"],
"state_transitions": ["proposal -> SANDBOXED task-packet candidate", "invalid scope/policy -> rejected result"],
"side_effects_and_privileges": ["No filesystem mutation, source checkout, secret access, provider call, deployment, promotion, or authority expansion"],
"persistence_and_migrations": ["No schema or migration changes"],
"failure_retry_restart": ["Deterministic validation is rerunnable; invalid or protected scope fails closed"],
"assumptions": ["A later external development control plane performs actual worktree creation", "Human governance remains promotion authority"],
"acceptance_scenarios": ["valid proposal produces scoped SANDBOXED descriptor", "absolute/traversal/protected path is rejected", "production and secret access policies must be DENIED", "missing provenance or test plan is rejected", "descriptor contains no command or credential material"],
"required_tests": ["go test ./modules/work/domain ./tests/semantic_evolution", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000187-scaffold-task-packet-candidate.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000187-scaffold-task-packet-candidate.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check", "CI"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
+59
View File
@@ -11,6 +11,7 @@ import (
var ErrInvalidDevelopmentGate = errors.New("invalid governed self-development gate")
var ErrInsufficientExtensionEvidence = errors.New("insufficient repeated extension evidence")
var ErrInvalidExtensionProposal = errors.New("invalid extension proposal")
var ErrInvalidDevelopmentScaffold = errors.New("invalid development scaffold candidate")
// BuildExtensionProposal deterministically turns a repeated evidence signal
// into a proposal. It retains every distinct evidence reference for lineage,
@@ -86,6 +87,64 @@ func cleanStrings(values []string) []string {
return result
}
// BuildDevelopmentScaffold creates only a typed, isolated candidate. The
// caller supplies the protected path set so this domain function cannot infer
// or widen repository authority from ambient process state.
func BuildDevelopmentScaffold(request work.DevelopmentScaffoldRequest) (work.DevelopmentScaffold, error) {
modules := cleanStrings(request.Modules)
allowed := cleanPaths(request.AllowedPaths)
forbidden := cleanPaths(request.ForbiddenPaths)
protected := cleanPaths(request.ProtectedPaths)
tests := cleanStrings(request.RequiredTests)
lineage := cleanStrings(request.ArtifactLineageRefs)
if request.ID == "" || request.ProposalRef == "" || strings.TrimSpace(request.BaseRepoRevision) == "" || strings.TrimSpace(request.WorktreeRef) == "" || strings.TrimSpace(request.TaskIntent) == "" || request.CreatedAt.IsZero() || len(modules) == 0 || len(allowed) == 0 || len(tests) == 0 || len(lineage) == 0 || request.ProtectedMainAccess != "DENIED" || request.ProductionAccess != "DENIED" || request.SecretAccess != "DENIED" {
return work.DevelopmentScaffold{}, ErrInvalidDevelopmentScaffold
}
for _, path := range allowed {
if unsafePath(path) || pathMatchesAny(path, forbidden) || pathMatchesAny(path, protected) {
return work.DevelopmentScaffold{}, ErrInvalidDevelopmentScaffold
}
}
worktree := strings.ToLower(strings.TrimSpace(request.WorktreeRef))
if worktree == "main" || worktree == "protected-main" || worktree == "production" {
return work.DevelopmentScaffold{}, ErrInvalidDevelopmentScaffold
}
return work.DevelopmentScaffold{ID: request.ID, Stage: work.DevelopmentScaffoldSandboxed, ProposalRef: request.ProposalRef, BaseRepoRevision: strings.TrimSpace(request.BaseRepoRevision), WorktreeRef: strings.TrimSpace(request.WorktreeRef), TaskIntent: strings.TrimSpace(request.TaskIntent), Modules: modules, AllowedPaths: allowed, ForbiddenPaths: forbidden, RequiredTests: tests, ArtifactLineageRefs: lineage, ProtectedMainAccess: "DENIED", ProductionAccess: "DENIED", SecretAccess: "DENIED", CreatedAtUnixMillis: request.CreatedAt.UnixMilli()}, nil
}
func cleanPaths(values []string) []string {
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(strings.ReplaceAll(value, "\\", "/"))
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
func unsafePath(path string) bool {
return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "~") || (len(path) > 1 && path[1] == ':') || slices.Contains(strings.Split(path, "/"), "..")
}
func pathMatchesAny(path string, scopes []string) bool {
for _, scope := range scopes {
scope = strings.TrimSuffix(scope, "/")
scope = strings.TrimSuffix(scope, "**")
scope = strings.TrimSuffix(scope, "/")
if path == scope || strings.HasPrefix(path, scope+"/") {
return true
}
}
return false
}
func AdvanceDevelopmentGate(current work.DevelopmentGate, next work.DevelopmentStage) (work.DevelopmentGate, error) {
if err := ValidateDevelopmentGate(current); err != nil {
return work.DevelopmentGate{}, err
@@ -51,6 +51,22 @@ func TestBuildExtensionProposalRejectsSingleFailureAndIncompleteGovernance(t *te
}
}
func TestBuildDevelopmentScaffoldFailsClosedForProtectedScope(t *testing.T) {
base := work.DevelopmentScaffoldRequest{ID: "scaffold-1", ProposalRef: "proposal-1", BaseRepoRevision: "abc123", WorktreeRef: "worktree-1", TaskIntent: "add reviewed adapter", Modules: []string{"capability"}, AllowedPaths: []string{"modules/capability/**"}, ForbiddenPaths: []string{"security/**"}, ProtectedPaths: []string{"architecture/**", "deploy/**"}, RequiredTests: []string{"go test ./..."}, ArtifactLineageRefs: []string{"proposal-1"}, ProtectedMainAccess: "DENIED", ProductionAccess: "DENIED", SecretAccess: "DENIED", CreatedAt: time.UnixMilli(1)}
scaffold, err := BuildDevelopmentScaffold(base)
if err != nil || scaffold.Stage != work.DevelopmentScaffoldSandboxed {
t.Fatalf("valid scaffold rejected: %+v %v", scaffold, err)
}
base.AllowedPaths = []string{"deploy/docker-compose.prod.yml"}
if _, err := BuildDevelopmentScaffold(base); err != ErrInvalidDevelopmentScaffold {
t.Fatalf("protected path must fail closed, got %v", err)
}
base.AllowedPaths = []string{"modules/capability/../security"}
if _, err := BuildDevelopmentScaffold(base); err != ErrInvalidDevelopmentScaffold {
t.Fatalf("traversal path must fail closed, got %v", err)
}
}
func TestGovernedSelfDevelopmentRejectsSameProducerAndVerifier(t *testing.T) {
gate := work.DevelopmentGate{ID: "dev-1", Stage: work.DevelopmentIsolated, EvidenceRefs: []string{"obs-1"}, ProducerRunRefs: []work.AgentRunID{"same"}, VerifierRunRefs: []work.AgentRunID{"same"}, VerificationEvidence: "verify-1"}
if _, err := AdvanceDevelopmentGate(gate, work.DevelopmentVerified); err == nil {
@@ -26,3 +26,10 @@ func TestExtensionProposalIsProposalOnlyAndDeduplicatesSignals(t *testing.T) {
t.Fatalf("unexpected proposal stage/keys: %+v", proposal)
}
}
func TestDevelopmentScaffoldRequiresExplicitDeniedAccessPolicies(t *testing.T) {
_, err := workdomain.BuildDevelopmentScaffold(work.DevelopmentScaffoldRequest{ID: "s", ProposalRef: "p", BaseRepoRevision: "r", WorktreeRef: "isolated", TaskIntent: "test", Modules: []string{"work"}, AllowedPaths: []string{"modules/work/**"}, RequiredTests: []string{"go test ./..."}, ArtifactLineageRefs: []string{"p"}, ProtectedMainAccess: "DENIED", ProductionAccess: "DENIED", SecretAccess: "ALLOW", CreatedAt: time.UnixMilli(1)})
if err == nil {
t.Fatal("secret access must be denied explicitly")
}
}