phase 1: Go foundation — module, migrations, domain, seed ingest
Core deliverables: - Go module github.com/dtoro/oikos (Go 1.26.3) - cmd/oikos: single binary with role subcommands (migrate, seed, export) - 6 SQL migrations: ontology meta-schema, entity instances (UUID+slug, blast_radius recursive function), operations (signals/checks/approvals), cognition (classifications/executions/feedback/patterns/skills), policy, observability (TimescaleDB hypertables + CAGGs + retention) - Domain layer: entity, signal, execution, classification, pattern, skill, approval, check types + 11 sentinel errors + lifecycle state machines - DB layer: pgx pool, SQL splitter (handles 94436 and -- comments), migration runner, seed ingest (ontology+inventory+policy) with content-hash dedup - Config: env-based with defaults, secrets redaction - Observability: slog JSON logger with debug mode - Infrastructure: Makefile, docker-compose.yml, multi-stage Dockerfile (distroless, CGO_ENABLED=0) Verified end-to-end against timescale/timescaledb:2.17.2-pg16: - 6 migrations applied (65 SQL statements) - Seeds ingested: 6 lifecycles, 59 entity types, 46 relationship types, 111 entities, 144 relationships, 4 risk classes, 27 approval rules, 9 autonomy settings - Idempotent: second seed run is a no-op (content hash matches) Bugs fixed during implementation: - TimescaleDB CAGGs can't run in a transaction -> splitSQL() executes statements individually - Semicolons in -- comments treated as separators -> comment handling - YAML keys source/target didn't match code's source_type/target_type - yaml.Marshal produced YAML for JSONB columns -> json.Marshal
This commit is contained in:
110
internal/domain/approval.go
Normal file
110
internal/domain/approval.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// Approval is a short-TTL signed grant for a gated action.
|
||||
type Approval struct {
|
||||
EntityID UUID
|
||||
SubjectEntityID UUID
|
||||
Action string
|
||||
RiskClass string
|
||||
Kind string
|
||||
Payload map[string]any
|
||||
Status string
|
||||
TokenHash string
|
||||
ExpiresAt time.Time
|
||||
DecidedAt *time.Time
|
||||
DecidedBy UUID
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Approval statuses.
|
||||
const (
|
||||
ApprovalPending = "pending"
|
||||
ApprovalApproved = "approved"
|
||||
ApprovalDenied = "denied"
|
||||
ApprovalExpired = "expired"
|
||||
ApprovalRevoked = "revoked"
|
||||
)
|
||||
|
||||
// Approval kinds.
|
||||
const (
|
||||
ApprovalKindExecution = "execution"
|
||||
ApprovalKindPolicyChange = "policy-change"
|
||||
ApprovalKindPatternActivation = "pattern-activation"
|
||||
)
|
||||
|
||||
// CheckDef defines a probe (R3-7: probes as data, not code).
|
||||
type CheckDef struct {
|
||||
EntityID UUID
|
||||
TargetID UUID
|
||||
TargetType string
|
||||
Kind string
|
||||
Config map[string]any
|
||||
IntervalS int
|
||||
TimeoutS int
|
||||
Zone string
|
||||
Enabled bool
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Check kinds.
|
||||
const (
|
||||
CheckHTTP = "http"
|
||||
CheckTCP = "tcp"
|
||||
CheckDisk = "disk"
|
||||
CheckCertExpiry = "cert-expiry"
|
||||
CheckDrift = "drift"
|
||||
CheckSSHScript = "ssh-script"
|
||||
)
|
||||
|
||||
// EntityStatus is the current health of an entity (R3-6: replaces state_snapshots).
|
||||
type EntityStatus struct {
|
||||
EntityID UUID
|
||||
Health string
|
||||
LastCheckAt *time.Time
|
||||
Details map[string]any
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Health values.
|
||||
const (
|
||||
HealthHealthy = "healthy"
|
||||
HealthDegraded = "degraded"
|
||||
HealthDown = "down"
|
||||
HealthUnknown = "unknown"
|
||||
)
|
||||
|
||||
// RiskClass is the four-level safety model.
|
||||
type RiskClass struct {
|
||||
Name string
|
||||
Description string
|
||||
ApprovalRequired string
|
||||
AutonomyAllowed bool
|
||||
}
|
||||
|
||||
// Risk class names.
|
||||
const (
|
||||
RiskReadOnly = "read_only"
|
||||
RiskReversibleLow = "reversible_low"
|
||||
RiskConfigMutation = "config_mutation"
|
||||
RiskDestructive = "destructive"
|
||||
)
|
||||
|
||||
// ApprovalRule maps (entity_type, action) → risk_class + autonomy.
|
||||
type ApprovalRule struct {
|
||||
ID UUID
|
||||
EntityType string
|
||||
Action string
|
||||
RiskClass string
|
||||
AutonomyLevel string
|
||||
ScopeEntity UUID
|
||||
Version int
|
||||
}
|
||||
|
||||
// Autonomy levels.
|
||||
const (
|
||||
AutonomyAuto = "auto"
|
||||
AutonomyEscalate = "escalate"
|
||||
AutonomyNever = "never"
|
||||
)
|
||||
76
internal/domain/entity.go
Normal file
76
internal/domain/entity.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Entity is the core graph node — every object in the OS is an entity.
|
||||
// Typed tables (signals, executions, etc.) reference entities(id) for
|
||||
// indexed querying; graph edges live in the relationships table.
|
||||
type Entity struct {
|
||||
ID UUID
|
||||
Slug string
|
||||
Type string
|
||||
Name string
|
||||
State string
|
||||
Attributes map[string]any
|
||||
MaintenanceUntil *time.Time
|
||||
Version int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// EntityType is the meta-schema entry defining what entities can exist.
|
||||
type EntityType struct {
|
||||
Name string
|
||||
ParentType string
|
||||
IsAbstract bool
|
||||
Domain string
|
||||
Layer string
|
||||
Description string
|
||||
LifecycleID string
|
||||
AttributeSchema map[string]any
|
||||
SchemaVersion int
|
||||
Status string
|
||||
}
|
||||
|
||||
// RelationshipType defines a typed edge between entity types.
|
||||
type RelationshipType struct {
|
||||
Name string
|
||||
Inverse string
|
||||
SourceType string
|
||||
TargetType string
|
||||
Cardinality string
|
||||
Description string
|
||||
}
|
||||
|
||||
// LifecycleDef is the state machine for an entity type.
|
||||
type LifecycleDef struct {
|
||||
ID string
|
||||
States []string
|
||||
DefaultState string
|
||||
TerminalStates []string
|
||||
Transitions map[string]map[string]TransitionReq
|
||||
}
|
||||
|
||||
// TransitionReq holds the named preconditions for a lifecycle transition.
|
||||
type TransitionReq struct {
|
||||
Requires []string `json:"requires"`
|
||||
}
|
||||
|
||||
// Relationship is a typed edge between two entities.
|
||||
type Relationship struct {
|
||||
SourceID UUID
|
||||
TargetID UUID
|
||||
Type string
|
||||
Attributes map[string]any
|
||||
ValidFrom time.Time
|
||||
ValidTo *time.Time
|
||||
}
|
||||
|
||||
// UUID is a type alias for UUID values. Using string for simplicity;
|
||||
// the DB layer uses pgx's UUID type. Conversion happens at the boundary.
|
||||
type UUID string
|
||||
|
||||
// IsNil returns true if the UUID is empty.
|
||||
func (u UUID) IsNil() bool { return u == "" }
|
||||
21
internal/domain/errors.go
Normal file
21
internal/domain/errors.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package domain
|
||||
|
||||
import "errors"
|
||||
|
||||
// Sentinel errors. Used throughout the codebase for typed error handling.
|
||||
// The API middleware maps these to HTTP status codes (SG11).
|
||||
var (
|
||||
ErrNotFound = errors.New("entity not found")
|
||||
ErrInvalidTransition = errors.New("invalid lifecycle transition")
|
||||
ErrApprovalRequired = errors.New("operator approval required")
|
||||
ErrAutonomyBlocked = errors.New("autonomy policy blocks this action")
|
||||
ErrConflict = errors.New("concurrent modification conflict")
|
||||
ErrCircuitOpen = errors.New("circuit breaker open for target")
|
||||
ErrAbstractType = errors.New("cannot instantiate abstract entity type")
|
||||
ErrInvalidEdge = errors.New("relationship endpoint type mismatch")
|
||||
ErrCardinality = errors.New("relationship cardinality violation")
|
||||
ErrSeedHashMismatch = errors.New("seed content hash mismatch")
|
||||
ErrAlreadyExists = errors.New("entity already exists")
|
||||
ErrQuarantined = errors.New("pattern is quarantined")
|
||||
ErrSkillDeprecated = errors.New("skill is deprecated")
|
||||
)
|
||||
64
internal/domain/execution.go
Normal file
64
internal/domain/execution.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// Classification persists every autonomous decision the classifier makes (SA5).
|
||||
// This is the audit trail for "why did the OS auto-act / escalate?"
|
||||
type Classification struct {
|
||||
EntityID UUID
|
||||
SignalEntityID UUID
|
||||
TargetEntityID UUID
|
||||
Action string
|
||||
RecommendedAction map[string]any
|
||||
RiskClass string
|
||||
Route string
|
||||
BlastRadius []UUID
|
||||
PatternConfidence float64
|
||||
SkillID UUID
|
||||
AutonomyCheck string
|
||||
Reasoning map[string]any
|
||||
CorrelationID string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Classification routes.
|
||||
const (
|
||||
RouteAutoAct = "auto-act"
|
||||
RouteEscalate = "escalate"
|
||||
RouteHold = "hold"
|
||||
)
|
||||
|
||||
// Execution is a detailed record of one action the OS performed.
|
||||
type Execution struct {
|
||||
EntityID UUID
|
||||
ClassificationID UUID
|
||||
SignalEntityID UUID
|
||||
TargetEntityID UUID
|
||||
Action string
|
||||
RiskClass string
|
||||
ApprovalID UUID
|
||||
AgentID UUID
|
||||
SkillID UUID
|
||||
SkillVersion int
|
||||
Status string
|
||||
Result map[string]any
|
||||
DurationMs int
|
||||
Verified bool
|
||||
CorrelationID string
|
||||
StartedAt *time.Time
|
||||
CompletedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Execution lifecycle states.
|
||||
const (
|
||||
ExecProposed = "proposed"
|
||||
ExecApproved = "approved"
|
||||
ExecExecuting = "executing"
|
||||
ExecVerified = "verified"
|
||||
ExecFailed = "failed"
|
||||
ExecTimedOut = "timed-out"
|
||||
ExecRolledBack = "rolled-back"
|
||||
ExecCancelled = "cancelled"
|
||||
ExecExpired = "expired"
|
||||
)
|
||||
76
internal/domain/pattern.go
Normal file
76
internal/domain/pattern.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// Feedback records what was learned from an execution.
|
||||
type Feedback struct {
|
||||
EntityID UUID
|
||||
ExecutionID UUID
|
||||
Outcome string
|
||||
Observation string
|
||||
Lesson string
|
||||
UnexpectedSideEffects []string
|
||||
Tags []string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Feedback outcomes.
|
||||
const (
|
||||
OutcomeSuccess = "success"
|
||||
OutcomeFailure = "failure"
|
||||
OutcomePartial = "partial"
|
||||
OutcomeUnexpected = "unexpected"
|
||||
)
|
||||
|
||||
// Pattern is a generalized rule extracted from accumulated feedback.
|
||||
type Pattern struct {
|
||||
EntityID UUID
|
||||
AppliesType string
|
||||
Action string
|
||||
Pattern string
|
||||
Confidence float64
|
||||
EvidenceCount int
|
||||
SuccessCount int
|
||||
FailureCount int
|
||||
Status string
|
||||
Quarantined bool
|
||||
Version int
|
||||
LastValidatedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Pattern lifecycle states.
|
||||
const (
|
||||
PatternHypothesized = "hypothesized"
|
||||
PatternValidated = "validated"
|
||||
PatternActive = "active"
|
||||
PatternDeprecated = "deprecated"
|
||||
PatternInvalidated = "invalidated"
|
||||
)
|
||||
|
||||
// Skill is a codified procedure refined through validated patterns.
|
||||
type Skill struct {
|
||||
EntityID UUID
|
||||
Version int
|
||||
Name string
|
||||
Procedure map[string]any
|
||||
AppliesType string
|
||||
Action string
|
||||
PatternIDs []UUID
|
||||
Status string
|
||||
SuccessRate float64
|
||||
ChangedBy UUID
|
||||
ChangeReason string
|
||||
LastUsedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Skill lifecycle states.
|
||||
const (
|
||||
SkillDrafted = "drafted"
|
||||
SkillTested = "tested"
|
||||
SkillActive = "active"
|
||||
SkillRefined = "refined"
|
||||
SkillDeprecated = "deprecated"
|
||||
SkillFailed = "failed"
|
||||
)
|
||||
65
internal/domain/signal.go
Normal file
65
internal/domain/signal.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// Signal is an attention record — something the lab noticed that needs
|
||||
// attention and possibly action. Dual entity: has an entities row + a
|
||||
// signals table row for indexed querying.
|
||||
type Signal struct {
|
||||
EntityID UUID
|
||||
Kind string
|
||||
Severity string
|
||||
TargetEntityID UUID
|
||||
CheckID UUID
|
||||
Evidence string
|
||||
LikelyCause string
|
||||
State string
|
||||
OccurrenceCount int
|
||||
FirstSeenAt time.Time
|
||||
LastSeenAt time.Time
|
||||
FlapCount int
|
||||
HoldDownUntil *time.Time
|
||||
MuteUntil *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Signal lifecycle states (see lifecycle_defs in seeds/ontology.yaml).
|
||||
const (
|
||||
SignalRaised = "raised"
|
||||
SignalAcknowledged = "acknowledged"
|
||||
SignalActing = "acting"
|
||||
SignalMuted = "muted"
|
||||
SignalResolved = "resolved"
|
||||
SignalFailed = "failed"
|
||||
)
|
||||
|
||||
// Signal severities.
|
||||
const (
|
||||
SeverityInfo = "info"
|
||||
SeverityWarning = "warning"
|
||||
SeverityCritical = "critical"
|
||||
)
|
||||
|
||||
// ValidSignalTransitions defines legal state transitions.
|
||||
var ValidSignalTransitions = map[string][]string{
|
||||
SignalRaised: {SignalAcknowledged, SignalMuted, SignalResolved},
|
||||
SignalAcknowledged: {SignalActing, SignalResolved, SignalMuted},
|
||||
SignalActing: {SignalResolved, SignalRaised, SignalFailed},
|
||||
SignalFailed: {SignalAcknowledged},
|
||||
SignalMuted: {SignalRaised},
|
||||
}
|
||||
|
||||
// CanTransition returns true if from→to is a legal signal state transition.
|
||||
func (s *Signal) CanTransition(to string) bool {
|
||||
allowed, ok := ValidSignalTransitions[s.State]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, a := range allowed {
|
||||
if a == to {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user