Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae5848d799 | ||
|
|
ec79f19a79 | ||
|
|
f63c4bb40c | ||
|
|
22f839df99 |
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
||||
)
|
||||
|
||||
var errDevelopmentGateCreate = errors.New("development gate must start at evidence collected with revision one")
|
||||
|
||||
func prepareDevelopmentGate(value work.DevelopmentGate, now time.Time) (work.DevelopmentGate, error) {
|
||||
if value.Stage != work.DevelopmentEvidenceCollected || value.Revision != 1 {
|
||||
return work.DevelopmentGate{}, errDevelopmentGateCreate
|
||||
}
|
||||
value.UpdatedAtUnixMillis = now.UnixMilli()
|
||||
if err := workdomain.ValidateDevelopmentGate(value); err != nil {
|
||||
return work.DevelopmentGate{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func advanceDevelopmentGate(value work.DevelopmentGate, next work.DevelopmentStage, now time.Time) (work.DevelopmentGate, error) {
|
||||
advanced, err := workdomain.AdvanceDevelopmentGate(value, next)
|
||||
if err != nil {
|
||||
return work.DevelopmentGate{}, err
|
||||
}
|
||||
advanced.Revision = value.Revision + 1
|
||||
advanced.UpdatedAtUnixMillis = now.UnixMilli()
|
||||
return advanced, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gate.ProposalRef = "proposal-1"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentProposalCreated, now)
|
||||
if err != nil || gate.Revision != 2 {
|
||||
t.Fatalf("proposal transition failed: %#v %v", gate, err)
|
||||
}
|
||||
gate.TaskPacketRef = "task-1"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentTaskAdmitted, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gate.WorktreeRef = "isolated-worktree"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentIsolated, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gate.ProducerRunRefs = []work.AgentRunID{"producer"}
|
||||
gate.VerifierRunRefs = []work.AgentRunID{"verifier"}
|
||||
gate.VerificationEvidence = "verification-1"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentVerified, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := advanceDevelopmentGate(gate, work.DevelopmentPromoted, now); err == nil {
|
||||
t.Fatal("promotion bypassed canary")
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,10 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
researchClient, err := newResearchRuntimeClient(envOr("RESEARCH_RUNTIME_URL", "http://research-runtime:8087"), &http.Client{Timeout: 12 * time.Second})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
|
||||
mux := http.NewServeMux()
|
||||
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
@@ -178,6 +182,25 @@ func main() {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(session)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/research/web", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var request epistemics.WebResearchRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid web research request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
result, err := researchClient.EvaluateWeb(r.Context(), request)
|
||||
if err != nil {
|
||||
http.Error(w, "web research unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/proposals", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
@@ -233,6 +256,61 @@ func main() {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(scaffold)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/gates", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var gate work.DevelopmentGate
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&gate); err != nil {
|
||||
http.Error(w, "invalid development gate", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
gate, err := prepareDevelopmentGate(gate, time.Now())
|
||||
if err != nil {
|
||||
http.Error(w, "development gate rejected", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, 0); err != nil {
|
||||
http.Error(w, "development gate conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(gate)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/gates/{id}/advance", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
ExpectedRevision int64 `json:"expected_revision"`
|
||||
NextStage work.DevelopmentStage `json:"next_stage"`
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil || request.ExpectedRevision < 1 || request.NextStage == "" {
|
||||
http.Error(w, "invalid development gate transition", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
gate, err := workRepository.LoadDevelopmentGate(r.Context(), r.PathValue("id"))
|
||||
if err != nil || gate.Revision != request.ExpectedRevision {
|
||||
http.Error(w, "development gate revision conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
gate, err = advanceDevelopmentGate(gate, request.NextStage, time.Now())
|
||||
if err != nil {
|
||||
http.Error(w, "development gate transition rejected", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, request.ExpectedRevision); err != nil {
|
||||
http.Error(w, "development gate conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(gate)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
type researchRuntimeClient struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newResearchRuntimeClient(rawURL string, client *http.Client) (researchRuntimeClient, error) {
|
||||
base := strings.TrimRight(strings.TrimSpace(rawURL), "/")
|
||||
if base == "" {
|
||||
return researchRuntimeClient{}, fmt.Errorf("research runtime URL is empty")
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return researchRuntimeClient{baseURL: base, client: client}, nil
|
||||
}
|
||||
|
||||
func (c researchRuntimeClient) EvaluateWeb(ctx context.Context, request epistemics.WebResearchRequest) (epistemics.ResearchResult, error) {
|
||||
payload, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/research/web/evaluate", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return epistemics.ResearchResult{}, fmt.Errorf("research runtime status %d", response.StatusCode)
|
||||
}
|
||||
var result epistemics.ResearchResult
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
func TestResearchRuntimeClientForwardsBoundedWebRequest(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/v1/research/web/evaluate" || r.Header.Get("Content-Type") != "application/json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var request epistemics.WebResearchRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.URL != "https://assistant.cin.su/healthz" {
|
||||
t.Fatalf("unexpected request body: %#v %v", request, err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(epistemics.ResearchResult{PlanID: request.Plan.ID, Findings: []epistemics.ResearchFinding{{PropositionKey: request.PropositionKey, State: epistemics.FindingSupported}}})
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := newResearchRuntimeClient(server.URL, server.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := client.EvaluateWeb(context.Background(), epistemics.WebResearchRequest{Plan: epistemics.ResearchPlan{ID: "plan-1"}, PropositionKey: "health", URL: "https://assistant.cin.su/healthz"})
|
||||
if err != nil || result.PlanID != "plan-1" || len(result.Findings) != 1 || result.Findings[0].State != epistemics.FindingSupported {
|
||||
t.Fatalf("unexpected result: %#v %v", result, err)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000228-controlled-discovery.json",
|
||||
"task_id": "DEV-000228",
|
||||
"task_file": "dev/tasks/DEV-000230-development-gate-controller.json",
|
||||
"task_id": "DEV-000230",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000229",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "22f839df99d8f6abd95f00cf99d34b4b45ace692",
|
||||
"risk_tier": "E",
|
||||
"intent": "Connect authenticated Domovoy/Core requests to the existing bounded Truth/Research web runtime while preserving source freshness, exact citations, conflicts and read-only connector policy.",
|
||||
"requirement_ids": ["Truth/Research runtime", "authenticated internet research", "source evidence"],
|
||||
"allowed_change_scope": {"modules": ["core-runtime", "research-runtime-bridge"], "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/research_client.go", "cmd/core-runtime/research_client_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000229-research-runtime-bridge.json", "docs/PROJECT_STATUS.md"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "contracts/**", "database/**", "deploy/**", "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": 0},
|
||||
"contracts": {"consume": ["existing epistemics.WebResearchRequest and ResearchResult", "existing authenticated Creator session"], "may_change": ["authenticated Core-to-research runtime adapter route"]},
|
||||
"semantic_owner": "epistemics research runtime; Core is an authenticated transport boundary",
|
||||
"canonical_state": ["epistemics runtime remains sole owner of persisted truth bundles, claims, beliefs and citations"],
|
||||
"state_transitions": ["authenticated request -> bounded web connector -> evaluated persisted finding"],
|
||||
"side_effects_and_privileges": ["read-only outbound source fetches to preconfigured allowlisted hosts; authenticated user request persists evidence in epistemics owner"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["research runtime unavailable returns explicit gateway failure; no claim is emitted by Core"],
|
||||
"assumptions": ["research runtime allowlist is configured outside code", "web connector retains exact source snapshot and does not invent citations"],
|
||||
"acceptance_scenarios": ["unauthenticated research request is denied", "authenticated request reaches research runtime", "source evidence and finding are returned without fabricated claims", "research runtime remains bounded by allowlist", "no secret is forwarded or returned"],
|
||||
"required_tests": ["go test ./cmd/core-runtime/...", "go vet ./cmd/core-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health and research-runtime readiness smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000230",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "ec79f19a7935e1eb8f0f59c4c9ce2940b97c4855",
|
||||
"risk_tier": "E",
|
||||
"intent": "Expose the existing Work-owned governed development state machine through authenticated optimistic-concurrency routes without executing code, changing protected source or bypassing promotion gates.",
|
||||
"requirement_ids": ["governed self-development", "durable gate progression", "canary rollback control"],
|
||||
"allowed_change_scope": {"modules": ["core-runtime", "governed-development"], "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/development_gate.go", "cmd/core-runtime/development_gate_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000230-development-gate-controller.json", "docs/PROJECT_STATUS.md"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "contracts/**", "database/**", "deploy/**", "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": 0},
|
||||
"contracts": {"consume": ["Work DevelopmentGate state machine", "authenticated Creator session", "optimistic revision persistence"], "may_change": ["authenticated Core gate controller routes"]},
|
||||
"semantic_owner": "Work governed development gate",
|
||||
"canonical_state": ["Work-owned development gate and existing proposal/scaffold records"],
|
||||
"state_transitions": ["EVIDENCE_COLLECTED -> PROPOSAL_CREATED -> TASK_ADMITTED -> ISOLATED -> VERIFIED -> CANARY -> PROMOTED", "CANARY -> ROLLED_BACK"],
|
||||
"side_effects_and_privileges": ["authenticated durable gate creation/transition only; no checkout, code execution, secret access, production mutation or automatic promotion"],
|
||||
"persistence_and_migrations": ["none; existing development_gates table and repository are reused"],
|
||||
"failure_retry_restart": ["optimistic revision conflict is explicit; invalid or skipped transitions are rejected; rollback remains available only from canary"],
|
||||
"assumptions": ["Creator authentication is the explicit governance boundary for these operator routes", "actual worktree execution and deployment remain separate controlled systems"],
|
||||
"acceptance_scenarios": ["gate creation requires evidence-collected revision one", "sequential transitions require their typed references", "verified cannot jump directly to promoted", "stale revision is rejected", "canary rollback is allowed", "unauthenticated requests are denied"],
|
||||
"required_tests": ["go test ./cmd/core-runtime/...", "go vet ./cmd/core-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
- `COMPLETION_MATRIX = docs/evidence/audit-completion-matrix-2026-08-23-v3.json`; all seven objective areas are mapped to current repository/production proof, with external bridge, GPU promotion/quality and deep-acceptance gaps explicit.
|
||||
|
||||
- `ACTIVE_TASK = dev/tasks/DEV-000228-controlled-discovery.json revision 1`; the cognitive worker now exposes a read-only provider/model discovery status view. Discovery does not qualify, activate or authorize a provider; those remain separate evidence-gated steps.
|
||||
- `ACTIVE_TASK = dev/tasks/DEV-000230-development-gate-controller.json revision 1`; authenticated Core now exposes Work-owned durable development gate creation and optimistic-concurrency progression. Code execution, protected-main access, automatic promotion and production mutation remain outside this controller.
|
||||
- `LINEAGE_DECISION = docs/GOVERNANCE_LINEAGE_DECISION_2026-08-22.md`; historical packets remain provenance and are not current authority.
|
||||
- `REGISTRY_STATUS = YAML and JSON semantic mirrors agree`
|
||||
- `PRODUCTION_DOMAIN = docker.cin.su`; shared docker-test is not used for this project.
|
||||
|
||||
Reference in New Issue
Block a user