phase 3: control loop — scheduler, actuator, learning, notifier, policy, API endpoints

Implemented the full OODA control loop:

Scheduler:
- Check_defs runner with bounded worker pool (errgroup)
- Signal dedup via partial unique index (UpsertSignal)
- Recovery auto-resolves open signals
- Metrics writing (InsertMetricSample) and entity_status updates
- Housekeeping (idempotency-key prune)
- Graceful shutdown via ctx cancellation

Actuator:
- Auto-act signal consumer with FOR UPDATE SKIP LOCKED pattern
- Per-target serialization with pg_advisory_xact_lock
- Circuit breaker per target host (N consecutive failures → open)
- Autonomy kill-switch (global.auto_act, never_auto_act.<slug>)
- Execution record lifecycle (proposed → running → completed)

Learning engine:
- Hourly feedback extraction past watermark
- Wilson score confidence lower bound (conservative for small N)
- Pattern status: hypothesized → validated (N≥5, confidence ≥0.7)
- Anomaly quarantine for burst feedback
- Cap confidence by sample_size/5 (nothing confident before 5 samples)

Notifier:
- Approval token generation (HMAC single-use, hashed at rest)
- Pending approval expiry detection
- DB rendezvous pattern (no service-to-service RPC)

Policy classifier:
- Risk class resolution from policy tables
- Autonomy checks (global + per-entity kill-switch)
- Blast radius computation
- Classification routes: auto-act / escalate / hold

API endpoints (31 endpoints implemented):
- Checks: ListChecks, CreateCheck, PatchCheck
- Classifications: ListClassifications
- Executions: ListExecutions, GetExecution, RequestExecution, CancelExecution
- Approvals: ListApprovals, DecideApproval
- Patterns: ListPatterns, PatchPattern
- Skills: ListSkills, PatchSkill, ListSkillVersions
- Policy: ListApprovalRules, CreateApprovalRule, PatchApprovalRule,
  GetAutonomySettings, PatchAutonomySettings, ListRiskClasses
- Relationships: CreateRelationship, EndRelationship
- Entity types: CreateEntityType, PatchEntityType
- Metrics: QueryMetrics, GetTrends
- Knowledge: SearchKnowledge, GetEntityKnowledge (stubs)
- Agent activity: QueryAgentActivity (stub)

Infrastructure:
- Migration 009: knowledge_entities table with FTS indexes
- Config: scheduler/notifier/actuator/learning env vars
- sqlc: 30+ new Phase 3 queries
- Integration tests for all new endpoints
- go.sum updated with golang.org/x/sync
This commit is contained in:
2026-07-07 15:19:25 +02:00
parent 7e802bbb14
commit 095a3967c4
17 changed files with 4692 additions and 150 deletions

View File

@@ -1,131 +1,6 @@
package httpapi
// Stubs for operations landing later in Phase 2/3. Each returns 501
// problem+json via errNotImplemented. Regenerate the list when the spec
// grows: the compiler enforces interface completeness either way.
import (
"context"
"github.com/dtoro/oikos/internal/httpapi/gen"
)
func (s *Server) QueryAgentActivity(ctx context.Context, request gen.QueryAgentActivityRequestObject) (gen.QueryAgentActivityResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) ListApprovals(ctx context.Context, request gen.ListApprovalsRequestObject) (gen.ListApprovalsResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) DecideApproval(ctx context.Context, request gen.DecideApprovalRequestObject) (gen.DecideApprovalResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) ListChecks(ctx context.Context, request gen.ListChecksRequestObject) (gen.ListChecksResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) CreateCheck(ctx context.Context, request gen.CreateCheckRequestObject) (gen.CreateCheckResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) PatchCheck(ctx context.Context, request gen.PatchCheckRequestObject) (gen.PatchCheckResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) ListClassifications(ctx context.Context, request gen.ListClassificationsRequestObject) (gen.ListClassificationsResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) ListExecutions(ctx context.Context, request gen.ListExecutionsRequestObject) (gen.ListExecutionsResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) RequestExecution(ctx context.Context, request gen.RequestExecutionRequestObject) (gen.RequestExecutionResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) GetExecution(ctx context.Context, request gen.GetExecutionRequestObject) (gen.GetExecutionResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) CancelExecution(ctx context.Context, request gen.CancelExecutionRequestObject) (gen.CancelExecutionResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) SearchKnowledge(ctx context.Context, request gen.SearchKnowledgeRequestObject) (gen.SearchKnowledgeResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) GetEntityKnowledge(ctx context.Context, request gen.GetEntityKnowledgeRequestObject) (gen.GetEntityKnowledgeResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) QueryMetrics(ctx context.Context, request gen.QueryMetricsRequestObject) (gen.QueryMetricsResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) CreateEntityType(ctx context.Context, request gen.CreateEntityTypeRequestObject) (gen.CreateEntityTypeResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) PatchEntityType(ctx context.Context, request gen.PatchEntityTypeRequestObject) (gen.PatchEntityTypeResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) ListPatterns(ctx context.Context, request gen.ListPatternsRequestObject) (gen.ListPatternsResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) PatchPattern(ctx context.Context, request gen.PatchPatternRequestObject) (gen.PatchPatternResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) ListSkills(ctx context.Context, request gen.ListSkillsRequestObject) (gen.ListSkillsResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) PatchSkill(ctx context.Context, request gen.PatchSkillRequestObject) (gen.PatchSkillResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) ListSkillVersions(ctx context.Context, request gen.ListSkillVersionsRequestObject) (gen.ListSkillVersionsResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) ListApprovalRules(ctx context.Context, request gen.ListApprovalRulesRequestObject) (gen.ListApprovalRulesResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) CreateApprovalRule(ctx context.Context, request gen.CreateApprovalRuleRequestObject) (gen.CreateApprovalRuleResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) PatchApprovalRule(ctx context.Context, request gen.PatchApprovalRuleRequestObject) (gen.PatchApprovalRuleResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) GetAutonomySettings(ctx context.Context, request gen.GetAutonomySettingsRequestObject) (gen.GetAutonomySettingsResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) PatchAutonomySettings(ctx context.Context, request gen.PatchAutonomySettingsRequestObject) (gen.PatchAutonomySettingsResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) ListRiskClasses(ctx context.Context, request gen.ListRiskClassesRequestObject) (gen.ListRiskClassesResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) EndRelationship(ctx context.Context, request gen.EndRelationshipRequestObject) (gen.EndRelationshipResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) CreateRelationship(ctx context.Context, request gen.CreateRelationshipRequestObject) (gen.CreateRelationshipResponseObject, error) {
return nil, errNotImplemented
}
func (s *Server) GetTrends(ctx context.Context, request gen.GetTrendsRequestObject) (gen.GetTrendsResponseObject, error) {
return nil, errNotImplemented
}
// Remaining stubs for endpoints that depend on tables not yet created
// (knowledge_entities, agent_activity). These are kept here because the
// phase3.go file already defines them; this file is deliberately empty.
// The stubs live in phase3.go as simple errNotImplemented returns.