From 54958d0902a25a9a346abf57c1a7f7400a0c1724 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 23 Aug 2026 13:26:41 +0300 Subject: [PATCH] feat(work): persist governed development candidates --- adapters/postgres/work_repository.go | 31 +++++++++-- cmd/core-runtime/main.go | 55 +++++++++++++++++++ contracts/work/self_development.go | 32 ++++++----- .../043_governed_development_candidates.sql | 45 +++++++++++++++ dev/ACTIVE_TASK.json | 4 +- ...15-governed-development-control-plane.json | 23 ++++++++ .../governed_development_schema_test.go | 20 +++++++ 7 files changed, 189 insertions(+), 21 deletions(-) create mode 100644 database/migrations/043_governed_development_candidates.sql create mode 100644 dev/tasks/DEV-000215-governed-development-control-plane.json create mode 100644 tests/semantic_evolution/governed_development_schema_test.go diff --git a/adapters/postgres/work_repository.go b/adapters/postgres/work_repository.go index ba429d2..1b27b73 100644 --- a/adapters/postgres/work_repository.go +++ b/adapters/postgres/work_repository.go @@ -18,10 +18,11 @@ import ( type WorkRepository struct{ pool *pgxpool.Pool } var ( - ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict") - ErrAgentLeaseUnavailable = errors.New("agent lease unavailable") - ErrAgentCallbackRejected = errors.New("agent callback rejected") - ErrDevelopmentGateConflict = errors.New("development gate revision conflict") + ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict") + ErrAgentLeaseUnavailable = errors.New("agent lease unavailable") + ErrAgentCallbackRejected = errors.New("agent callback rejected") + ErrDevelopmentGateConflict = errors.New("development gate revision conflict") + ErrDevelopmentCandidateConflict = errors.New("development candidate conflict") ) func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) { @@ -104,6 +105,28 @@ func (r *WorkRepository) SaveDevelopmentGate(ctx context.Context, gate work.Deve return nil } +func (r *WorkRepository) SaveExtensionProposal(ctx context.Context, value work.ExtensionProposal) error { + result, err := r.pool.Exec(ctx, `INSERT INTO work.extension_proposals(proposal_id,requested_by_principal_id,stage,evidence_refs,deduplicated_signal_keys,desired_outcome,smallest_change_layer,affected_owners,affected_contracts,risk_tier,evaluation_plan,alternatives,rollback_plan,assumptions,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT(proposal_id) DO NOTHING`, value.ID, value.RequestedByPrincipalID, value.Stage, value.EvidenceRefs, value.DeduplicatedSignalKeys, value.DesiredOutcome, value.SmallestChangeLayer, value.AffectedOwners, value.AffectedContracts, value.RiskTier, value.EvaluationPlan, value.Alternatives, value.RollbackPlan, value.Assumptions, value.CreatedAtUnixMillis) + if err != nil { + return err + } + if result.RowsAffected() != 1 { + return ErrDevelopmentCandidateConflict + } + return nil +} + +func (r *WorkRepository) SaveDevelopmentScaffold(ctx context.Context, value work.DevelopmentScaffold) error { + result, err := r.pool.Exec(ctx, `INSERT INTO work.development_scaffolds(scaffold_id,requested_by_principal_id,stage,proposal_ref,base_repo_revision,worktree_ref,task_intent,modules,allowed_paths,forbidden_paths,required_tests,artifact_lineage_refs,protected_main_access,production_access,secret_access,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT(scaffold_id) DO NOTHING`, value.ID, value.RequestedByPrincipalID, value.Stage, value.ProposalRef, value.BaseRepoRevision, value.WorktreeRef, value.TaskIntent, value.Modules, value.AllowedPaths, value.ForbiddenPaths, value.RequiredTests, value.ArtifactLineageRefs, value.ProtectedMainAccess, value.ProductionAccess, value.SecretAccess, value.CreatedAtUnixMillis) + if err != nil { + return err + } + if result.RowsAffected() != 1 { + return ErrDevelopmentCandidateConflict + } + return nil +} + func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.AgentDispatchIntent) error { tx, err := r.pool.Begin(ctx) if err != nil { diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 0f96a40..f5059b6 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -178,6 +178,61 @@ func main() { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(session) }) + mux.HandleFunc("POST /v1/creator/development/proposals", func(w http.ResponseWriter, r *http.Request) { + session, ok := authenticateCreator(w, r) + if !ok { + return + } + var request struct { + Evidence []work.ExtensionEvidence `json:"evidence"` + Proposal work.ExtensionProposalRequest `json:"proposal"` + } + decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&request); err != nil { + http.Error(w, "invalid development proposal", http.StatusBadRequest) + return + } + proposal, err := workdomain.BuildExtensionProposal(request.Evidence, request.Proposal) + if err != nil { + http.Error(w, "development proposal rejected", http.StatusUnprocessableEntity) + return + } + proposal.RequestedByPrincipalID = string(session.PrincipalID) + if err := workRepository.SaveExtensionProposal(r.Context(), proposal); err != nil { + http.Error(w, "development proposal conflict", http.StatusConflict) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(proposal) + }) + mux.HandleFunc("POST /v1/creator/development/scaffolds", func(w http.ResponseWriter, r *http.Request) { + session, ok := authenticateCreator(w, r) + if !ok { + return + } + var request work.DevelopmentScaffoldRequest + decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&request); err != nil { + http.Error(w, "invalid development scaffold", http.StatusBadRequest) + return + } + scaffold, err := workdomain.BuildDevelopmentScaffold(request) + if err != nil { + http.Error(w, "development scaffold rejected", http.StatusUnprocessableEntity) + return + } + scaffold.RequestedByPrincipalID = string(session.PrincipalID) + if err := workRepository.SaveDevelopmentScaffold(r.Context(), scaffold); err != nil { + http.Error(w, "development scaffold conflict", http.StatusConflict) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(scaffold) + }) mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) { session, ok := authenticateCreator(w, r) if !ok { diff --git a/contracts/work/self_development.go b/contracts/work/self_development.go index 872641f..fbf371e 100644 --- a/contracts/work/self_development.go +++ b/contracts/work/self_development.go @@ -70,6 +70,7 @@ type ExtensionProposalRequest struct { // authority grant, implementation body or promotion decision. type ExtensionProposal struct { ID string + RequestedByPrincipalID string Stage ExtensionProposalStage EvidenceRefs []string DeduplicatedSignalKeys []string @@ -113,19 +114,20 @@ type DevelopmentScaffoldRequest struct { // 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 + ID string + RequestedByPrincipalID 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 } diff --git a/database/migrations/043_governed_development_candidates.sql b/database/migrations/043_governed_development_candidates.sql new file mode 100644 index 0000000..c214ac6 --- /dev/null +++ b/database/migrations/043_governed_development_candidates.sql @@ -0,0 +1,45 @@ +BEGIN; + +SET ROLE work_role; + +CREATE TABLE IF NOT EXISTS work.extension_proposals ( + proposal_id TEXT PRIMARY KEY, + requested_by_principal_id TEXT NOT NULL, + stage TEXT NOT NULL, + evidence_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + deduplicated_signal_keys TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + desired_outcome TEXT NOT NULL, + smallest_change_layer TEXT NOT NULL, + affected_owners TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + affected_contracts TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + risk_tier TEXT NOT NULL, + evaluation_plan TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + alternatives TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + rollback_plan TEXT NOT NULL, + assumptions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + created_at_unix_millis BIGINT NOT NULL, + CHECK (proposal_id <> ''), CHECK (requested_by_principal_id <> ''), CHECK (stage = 'PROPOSED'), CHECK (desired_outcome <> ''), CHECK (smallest_change_layer <> ''), CHECK (risk_tier <> ''), CHECK (rollback_plan <> ''), CHECK (created_at_unix_millis > 0) +); + +CREATE TABLE IF NOT EXISTS work.development_scaffolds ( + scaffold_id TEXT PRIMARY KEY, + requested_by_principal_id TEXT NOT NULL, + stage TEXT NOT NULL, + proposal_ref TEXT NOT NULL, + base_repo_revision TEXT NOT NULL, + worktree_ref TEXT NOT NULL, + task_intent TEXT NOT NULL, + modules TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + allowed_paths TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + forbidden_paths TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + required_tests TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + artifact_lineage_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + protected_main_access TEXT NOT NULL, + production_access TEXT NOT NULL, + secret_access TEXT NOT NULL, + created_at_unix_millis BIGINT NOT NULL, + CHECK (scaffold_id <> ''), CHECK (requested_by_principal_id <> ''), CHECK (stage = 'SANDBOXED'), CHECK (proposal_ref <> ''), CHECK (base_repo_revision <> ''), CHECK (worktree_ref <> ''), CHECK (task_intent <> ''), CHECK (protected_main_access = 'DENIED'), CHECK (production_access = 'DENIED'), CHECK (secret_access = 'DENIED'), CHECK (created_at_unix_millis > 0) +); + +RESET ROLE; +COMMIT; diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 6d3b366..300181d 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000214-agent-execution-recovery.json", - "task_id": "DEV-000214", + "task_file": "dev/tasks/DEV-000215-governed-development-control-plane.json", + "task_id": "DEV-000215", "task_revision": 1 } diff --git a/dev/tasks/DEV-000215-governed-development-control-plane.json b/dev/tasks/DEV-000215-governed-development-control-plane.json new file mode 100644 index 0000000..f1a1980 --- /dev/null +++ b/dev/tasks/DEV-000215-governed-development-control-plane.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000215", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "8d3e7f7cac236aa50919bd1cd854012dcc8500a5", + "risk_tier": "E", + "intent": "Expose a durable, authenticated control-plane boundary for governed self-development proposals and isolated scaffold candidates without executing source mutation, bypassing verification, or granting promotion authority.", + "requirement_ids": ["durable proposal intake", "isolated scaffold contract", "authenticated operator boundary", "no implicit promotion"], + "allowed_change_scope": {"modules": ["work", "core-runtime"], "paths": ["contracts/work/self_development.go", "modules/work/domain/**", "cmd/core-runtime/main.go", "adapters/postgres/work_repository.go", "database/migrations/043_governed_development_candidates.sql", "tests/semantic_evolution/**", "tests/work/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000215-governed-development-control-plane.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "modules/identity/**", "modules/capability/**", "LLM-local/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1}, + "contracts": {"consume": ["ADR-0022 governed self-development boundary", "creator authentication", "work development gate"], "may_change": ["work-owned proposal/scaffold persistence and authenticated control-plane endpoints"]}, + "semantic_owner": "work owns proposal, scaffold and development gate lineage", + "canonical_state": ["work-owned extension proposal and scaffold candidates are durable; source tree and production remain outside this task"], + "state_transitions": ["repeated evidence -> PROPOSED", "proposal -> SANDBOXED scaffold candidate", "gate remains explicit and cannot be promoted by these endpoints"], + "side_effects_and_privileges": ["authenticated creator session required; database writes stay within work owner; no checkout, source mutation, deployment or secret access"], + "persistence_and_migrations": ["043 adds durable work-owned proposal/scaffold candidate records"], + "failure_retry_restart": ["idempotent candidate IDs preserve lineage; duplicate IDs are rejected; no endpoint advances promotion stage"], + "assumptions": ["an independent executor and verifier remain separate future capabilities"], + "acceptance_scenarios": ["unauthenticated requests are rejected", "repeated evidence creates a durable proposal", "protected paths cannot create a scaffold", "valid scaffold is durable and remains SANDBOXED", "no promotion or source mutation occurs"], + "required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "migration/schema checks", "CI", "production public health check"], + "terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"] +} diff --git a/tests/semantic_evolution/governed_development_schema_test.go b/tests/semantic_evolution/governed_development_schema_test.go new file mode 100644 index 0000000..bb078e3 --- /dev/null +++ b/tests/semantic_evolution/governed_development_schema_test.go @@ -0,0 +1,20 @@ +package semantic_evolution_test + +import ( + "os" + "strings" + "testing" +) + +func TestGovernedDevelopmentCandidateMigrationIsWorkOwnedAndSandboxed(t *testing.T) { + payload, err := os.ReadFile("../../database/migrations/043_governed_development_candidates.sql") + if err != nil { + t.Fatal(err) + } + source := strings.ToLower(string(payload)) + for _, fragment := range []string{"set role work_role", "create table if not exists work.extension_proposals", "create table if not exists work.development_scaffolds", "stage = 'sandboxed'", "protected_main_access = 'denied'", "production_access = 'denied'", "secret_access = 'denied'"} { + if !strings.Contains(source, fragment) { + t.Fatalf("migration is missing %q", fragment) + } + } +} -- 2.54.0