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:
43
migrations/001_ontology.up.sql
Normal file
43
migrations/001_ontology.up.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- Migration 001: Ontology meta-schema (with inheritance, R3-1)
|
||||
-- Defines entity types, relationship types, lifecycle definitions, and seed versioning.
|
||||
|
||||
CREATE TABLE lifecycle_defs (
|
||||
id TEXT PRIMARY KEY,
|
||||
states TEXT[] NOT NULL,
|
||||
default_state TEXT NOT NULL,
|
||||
terminal_states TEXT[] NOT NULL DEFAULT '{}',
|
||||
transitions JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE entity_types (
|
||||
name TEXT PRIMARY KEY,
|
||||
parent_type TEXT REFERENCES entity_types(name),
|
||||
is_abstract BOOLEAN NOT NULL DEFAULT false,
|
||||
domain TEXT NOT NULL,
|
||||
layer TEXT NOT NULL CHECK (layer IN ('meta','infrastructure','governance','cognition')),
|
||||
description TEXT,
|
||||
lifecycle_id TEXT REFERENCES lifecycle_defs(id),
|
||||
attribute_schema JSONB,
|
||||
schema_version INTEGER NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE relationship_types (
|
||||
name TEXT PRIMARY KEY,
|
||||
inverse TEXT,
|
||||
source_type TEXT NOT NULL REFERENCES entity_types(name),
|
||||
target_type TEXT NOT NULL REFERENCES entity_types(name),
|
||||
cardinality TEXT NOT NULL CHECK (cardinality IN
|
||||
('one-to-one','one-to-many','many-to-one','many-to-many')),
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE seed_versions (
|
||||
file TEXT PRIMARY KEY,
|
||||
content_hash TEXT NOT NULL,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
50
migrations/002_entities.up.sql
Normal file
50
migrations/002_entities.up.sql
Normal file
@@ -0,0 +1,50 @@
|
||||
-- Migration 002: Entity instances (UUIDv7 + slug, R3-5/D1)
|
||||
-- The inventory graph: entities + typed relationships + blast_radius function.
|
||||
|
||||
CREATE TABLE entities (
|
||||
id UUID PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL REFERENCES entity_types(name),
|
||||
name TEXT NOT NULL,
|
||||
state TEXT,
|
||||
attributes JSONB NOT NULL DEFAULT '{}',
|
||||
maintenance_until TIMESTAMPTZ,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (type, name)
|
||||
);
|
||||
CREATE INDEX idx_entities_type ON entities(type);
|
||||
CREATE INDEX idx_entities_state ON entities(state);
|
||||
CREATE INDEX idx_entities_attrs ON entities USING GIN(attributes);
|
||||
|
||||
CREATE TABLE relationships (
|
||||
source_id UUID NOT NULL REFERENCES entities(id) ON DELETE RESTRICT,
|
||||
target_id UUID NOT NULL REFERENCES entities(id) ON DELETE RESTRICT,
|
||||
type TEXT NOT NULL REFERENCES relationship_types(name),
|
||||
attributes JSONB,
|
||||
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
valid_to TIMESTAMPTZ,
|
||||
PRIMARY KEY (source_id, target_id, type, valid_from)
|
||||
);
|
||||
CREATE INDEX idx_rel_source ON relationships(source_id) WHERE valid_to IS NULL;
|
||||
CREATE INDEX idx_rel_target ON relationships(target_id) WHERE valid_to IS NULL;
|
||||
CREATE INDEX idx_rel_type ON relationships(type) WHERE valid_to IS NULL;
|
||||
|
||||
-- Cycle-safe traversal (P1): path accumulator prevents revisits; depth capped.
|
||||
CREATE OR REPLACE FUNCTION blast_radius(start_id UUID, max_depth INT DEFAULT 3,
|
||||
rel_types TEXT[] DEFAULT NULL)
|
||||
RETURNS TABLE(entity_id UUID, depth INT) AS $$
|
||||
WITH RECURSIVE walk AS (
|
||||
SELECT start_id AS entity_id, 0 AS depth, ARRAY[start_id] AS path
|
||||
UNION ALL
|
||||
SELECT r.target_id, w.depth + 1, w.path || r.target_id
|
||||
FROM relationships r
|
||||
JOIN walk w ON r.source_id = w.entity_id
|
||||
WHERE w.depth < LEAST(max_depth, 5)
|
||||
AND r.valid_to IS NULL
|
||||
AND NOT r.target_id = ANY(w.path)
|
||||
AND (rel_types IS NULL OR r.type = ANY(rel_types))
|
||||
)
|
||||
SELECT entity_id, MIN(depth) FROM walk GROUP BY entity_id;
|
||||
$$ LANGUAGE sql STABLE;
|
||||
71
migrations/003_operations.up.sql
Normal file
71
migrations/003_operations.up.sql
Normal file
@@ -0,0 +1,71 @@
|
||||
-- Migration 003: Operations (signals, checks, approvals, status)
|
||||
-- Signals are dual entities (entities row + signals table for indexed querying).
|
||||
|
||||
CREATE TABLE check_defs (
|
||||
entity_id UUID PRIMARY KEY REFERENCES entities(id),
|
||||
target_id UUID REFERENCES entities(id),
|
||||
target_type TEXT REFERENCES entity_types(name),
|
||||
kind TEXT NOT NULL,
|
||||
config JSONB NOT NULL DEFAULT '{}',
|
||||
interval_s INTEGER NOT NULL DEFAULT 600,
|
||||
timeout_s INTEGER NOT NULL DEFAULT 10,
|
||||
zone TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE signals (
|
||||
entity_id UUID PRIMARY KEY REFERENCES entities(id),
|
||||
kind TEXT NOT NULL,
|
||||
severity TEXT NOT NULL CHECK (severity IN ('info','warning','critical')),
|
||||
target_entity_id UUID REFERENCES entities(id),
|
||||
check_id UUID REFERENCES check_defs(entity_id),
|
||||
evidence TEXT,
|
||||
likely_cause TEXT,
|
||||
state TEXT NOT NULL DEFAULT 'raised',
|
||||
occurrence_count INTEGER NOT NULL DEFAULT 1,
|
||||
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
flap_count INTEGER NOT NULL DEFAULT 0,
|
||||
hold_down_until TIMESTAMPTZ,
|
||||
mute_until TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
-- At most ONE open signal per (target, kind) — repeats update the open row
|
||||
CREATE UNIQUE INDEX uq_signals_open ON signals(target_entity_id, kind)
|
||||
WHERE state NOT IN ('resolved','failed');
|
||||
CREATE INDEX idx_signals_state ON signals(state);
|
||||
|
||||
CREATE TABLE approvals (
|
||||
entity_id UUID PRIMARY KEY REFERENCES entities(id),
|
||||
subject_entity_id UUID REFERENCES entities(id),
|
||||
action TEXT NOT NULL,
|
||||
risk_class TEXT NOT NULL,
|
||||
kind TEXT NOT NULL DEFAULT 'execution',
|
||||
payload JSONB,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
token_hash TEXT,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
decided_at TIMESTAMPTZ,
|
||||
decided_by UUID REFERENCES entities(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE entity_status (
|
||||
entity_id UUID PRIMARY KEY REFERENCES entities(id),
|
||||
health TEXT NOT NULL DEFAULT 'unknown',
|
||||
last_check_at TIMESTAMPTZ,
|
||||
details JSONB NOT NULL DEFAULT '{}',
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE idempotency_keys (
|
||||
key TEXT NOT NULL,
|
||||
actor TEXT NOT NULL,
|
||||
request_hash TEXT NOT NULL,
|
||||
response_code INTEGER,
|
||||
response_body JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (actor, key)
|
||||
);
|
||||
91
migrations/004_cognition.up.sql
Normal file
91
migrations/004_cognition.up.sql
Normal file
@@ -0,0 +1,91 @@
|
||||
-- Migration 004: Cognition (classifications, executions, learning)
|
||||
-- All cognition objects are dual entities (entities row + typed table).
|
||||
-- Classifications persist every autonomous decision (SA5).
|
||||
|
||||
CREATE TABLE classifications (
|
||||
entity_id UUID PRIMARY KEY REFERENCES entities(id),
|
||||
signal_entity_id UUID REFERENCES signals(entity_id),
|
||||
target_entity_id UUID REFERENCES entities(id),
|
||||
action TEXT NOT NULL,
|
||||
recommended_action JSONB,
|
||||
risk_class TEXT NOT NULL,
|
||||
route TEXT NOT NULL CHECK (route IN ('auto-act','escalate','hold')),
|
||||
blast_radius UUID[],
|
||||
pattern_confidence REAL,
|
||||
skill_id UUID,
|
||||
autonomy_check TEXT,
|
||||
reasoning JSONB NOT NULL,
|
||||
correlation_id TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX idx_class_signal ON classifications(signal_entity_id);
|
||||
CREATE INDEX idx_class_entity ON classifications(target_entity_id);
|
||||
|
||||
CREATE TABLE executions (
|
||||
entity_id UUID PRIMARY KEY REFERENCES entities(id),
|
||||
classification_id UUID REFERENCES classifications(entity_id),
|
||||
signal_entity_id UUID REFERENCES signals(entity_id),
|
||||
target_entity_id UUID REFERENCES entities(id),
|
||||
action TEXT NOT NULL,
|
||||
risk_class TEXT NOT NULL,
|
||||
approval_id UUID REFERENCES approvals(entity_id),
|
||||
agent_id UUID REFERENCES entities(id),
|
||||
skill_id UUID,
|
||||
skill_version INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'proposed',
|
||||
result JSONB,
|
||||
duration_ms INTEGER,
|
||||
verified BOOLEAN NOT NULL DEFAULT false,
|
||||
correlation_id TEXT NOT NULL,
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX idx_exec_target ON executions(target_entity_id);
|
||||
CREATE INDEX idx_exec_status ON executions(status);
|
||||
|
||||
CREATE TABLE feedback (
|
||||
entity_id UUID PRIMARY KEY REFERENCES entities(id),
|
||||
execution_id UUID NOT NULL REFERENCES executions(entity_id),
|
||||
outcome TEXT NOT NULL CHECK (outcome IN ('success','failure','partial','unexpected')),
|
||||
observation TEXT,
|
||||
lesson TEXT,
|
||||
unexpected_side_effects TEXT[],
|
||||
tags TEXT[],
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX idx_feedback_ts ON feedback(created_at);
|
||||
|
||||
CREATE TABLE patterns (
|
||||
entity_id UUID PRIMARY KEY REFERENCES entities(id),
|
||||
applies_type TEXT NOT NULL REFERENCES entity_types(name),
|
||||
action TEXT NOT NULL,
|
||||
pattern TEXT NOT NULL,
|
||||
confidence REAL NOT NULL DEFAULT 0,
|
||||
evidence_count INTEGER NOT NULL DEFAULT 0,
|
||||
success_count INTEGER NOT NULL DEFAULT 0,
|
||||
failure_count INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'hypothesized',
|
||||
quarantined BOOLEAN NOT NULL DEFAULT false,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
last_validated_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (applies_type, action)
|
||||
);
|
||||
|
||||
CREATE TABLE skills (
|
||||
entity_id UUID NOT NULL REFERENCES entities(id),
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
name TEXT NOT NULL,
|
||||
procedure JSONB NOT NULL,
|
||||
applies_type TEXT REFERENCES entity_types(name),
|
||||
action TEXT NOT NULL,
|
||||
pattern_ids UUID[],
|
||||
status TEXT NOT NULL DEFAULT 'drafted',
|
||||
success_rate REAL,
|
||||
changed_by UUID,
|
||||
change_reason TEXT,
|
||||
last_used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (entity_id, version)
|
||||
);
|
||||
28
migrations/005_policy.up.sql
Normal file
28
migrations/005_policy.up.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- Migration 005: Policy (risk classes, approval rules, autonomy settings)
|
||||
|
||||
CREATE TABLE risk_classes (
|
||||
name TEXT PRIMARY KEY,
|
||||
description TEXT,
|
||||
approval_required TEXT NOT NULL DEFAULT 'none',
|
||||
autonomy_allowed BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
CREATE TABLE approval_rules (
|
||||
id UUID PRIMARY KEY,
|
||||
entity_type TEXT REFERENCES entity_types(name),
|
||||
action TEXT NOT NULL,
|
||||
risk_class TEXT NOT NULL REFERENCES risk_classes(name),
|
||||
autonomy_level TEXT NOT NULL DEFAULT 'escalate' CHECK
|
||||
(autonomy_level IN ('auto','escalate','never')),
|
||||
scope_entity UUID REFERENCES entities(id),
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (entity_type, action, scope_entity)
|
||||
);
|
||||
|
||||
CREATE TABLE autonomy_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
138
migrations/006_observability.up.sql
Normal file
138
migrations/006_observability.up.sql
Normal file
@@ -0,0 +1,138 @@
|
||||
-- Migration 006: Observability (TimescaleDB)
|
||||
-- Hypertable PKs include time column (SG1); idempotent DDL (SG3); no array_agg in CAGGs (SG2).
|
||||
|
||||
-- Enable TimescaleDB extension
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
|
||||
-- ─── Time-series metrics ──────────────────────────────────────────────
|
||||
CREATE TABLE metric_samples (
|
||||
ts TIMESTAMPTZ NOT NULL,
|
||||
entity_id UUID NOT NULL,
|
||||
metric TEXT NOT NULL,
|
||||
value DOUBLE PRECISION NOT NULL,
|
||||
tags JSONB NOT NULL DEFAULT '{}'
|
||||
);
|
||||
SELECT create_hypertable('metric_samples', 'ts',
|
||||
chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE);
|
||||
CREATE INDEX idx_metrics_entity_ts ON metric_samples(entity_id, ts DESC);
|
||||
CREATE INDEX idx_metrics_metric_ts ON metric_samples(metric, ts DESC);
|
||||
DO $$ BEGIN
|
||||
PERFORM add_retention_policy('metric_samples', INTERVAL '90 days');
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- 1-hour rollups
|
||||
CREATE MATERIALIZED VIEW metric_rollups_1h WITH (timescaledb.continuous) AS
|
||||
SELECT time_bucket('1 hour', ts) AS bucket, entity_id, metric,
|
||||
avg(value) AS avg_value, min(value) AS min_value,
|
||||
max(value) AS max_value, count(*) AS sample_count
|
||||
FROM metric_samples GROUP BY bucket, entity_id, metric;
|
||||
DO $$ BEGIN
|
||||
PERFORM add_continuous_aggregate_policy('metric_rollups_1h',
|
||||
start_offset => INTERVAL '2 hours',
|
||||
end_offset => INTERVAL '5 minutes',
|
||||
schedule_interval => INTERVAL '1 hour');
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- 1-day rollups
|
||||
CREATE MATERIALIZED VIEW metric_rollups_1d WITH (timescaledb.continuous) AS
|
||||
SELECT time_bucket('1 day', ts) AS bucket, entity_id, metric,
|
||||
avg(value) AS avg_value, min(value) AS min_value,
|
||||
max(value) AS max_value, count(*) AS sample_count
|
||||
FROM metric_samples GROUP BY bucket, entity_id, metric;
|
||||
DO $$ BEGIN
|
||||
PERFORM add_continuous_aggregate_policy('metric_rollups_1d',
|
||||
start_offset => INTERVAL '2 days',
|
||||
end_offset => INTERVAL '1 hour',
|
||||
schedule_interval => INTERVAL '1 day');
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- ─── Audit log ────────────────────────────────────────────────────────
|
||||
CREATE TABLE audit_log (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY,
|
||||
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
actor_type TEXT NOT NULL,
|
||||
actor_id UUID,
|
||||
action TEXT NOT NULL,
|
||||
entity_id UUID,
|
||||
method TEXT,
|
||||
path TEXT,
|
||||
status_code INTEGER,
|
||||
detail JSONB NOT NULL DEFAULT '{}',
|
||||
source_ip TEXT,
|
||||
correlation_id TEXT,
|
||||
PRIMARY KEY (id, ts)
|
||||
);
|
||||
SELECT create_hypertable('audit_log', 'ts',
|
||||
chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE);
|
||||
CREATE INDEX idx_audit_actor ON audit_log(actor_type, actor_id, ts DESC);
|
||||
CREATE INDEX idx_audit_entity ON audit_log(entity_id, ts DESC);
|
||||
CREATE INDEX idx_audit_action ON audit_log(action, ts DESC);
|
||||
CREATE INDEX idx_audit_correlation ON audit_log(correlation_id);
|
||||
DO $$ BEGIN
|
||||
PERFORM add_retention_policy('audit_log', INTERVAL '365 days');
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- ─── Event log ────────────────────────────────────────────────────────
|
||||
CREATE TABLE events (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY,
|
||||
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
type TEXT NOT NULL,
|
||||
entity_id UUID,
|
||||
severity TEXT NOT NULL DEFAULT 'info',
|
||||
source TEXT NOT NULL,
|
||||
data JSONB NOT NULL DEFAULT '{}',
|
||||
correlation_id TEXT,
|
||||
PRIMARY KEY (id, ts)
|
||||
);
|
||||
SELECT create_hypertable('events', 'ts',
|
||||
chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE);
|
||||
CREATE INDEX idx_events_type_ts ON events(type, ts DESC);
|
||||
CREATE INDEX idx_events_entity_ts ON events(entity_id, ts DESC);
|
||||
CREATE INDEX idx_events_severity_ts ON events(severity, ts DESC);
|
||||
CREATE INDEX idx_events_correlation ON events(correlation_id);
|
||||
DO $$ BEGIN
|
||||
PERFORM add_retention_policy('events', INTERVAL '90 days');
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- ─── Agent activity ──────────────────────────────────────────────────
|
||||
CREATE TABLE agent_activity (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY,
|
||||
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
agent_id UUID NOT NULL,
|
||||
session_id TEXT,
|
||||
activity_type TEXT NOT NULL,
|
||||
tool_name TEXT,
|
||||
entity_id UUID,
|
||||
input_summary TEXT,
|
||||
output_summary TEXT,
|
||||
duration_ms INTEGER,
|
||||
token_count INTEGER,
|
||||
success BOOLEAN,
|
||||
correlation_id TEXT,
|
||||
PRIMARY KEY (id, ts)
|
||||
);
|
||||
SELECT create_hypertable('agent_activity', 'ts',
|
||||
chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE);
|
||||
CREATE INDEX idx_agent_activity_agent_ts ON agent_activity(agent_id, ts DESC);
|
||||
CREATE INDEX idx_agent_activity_type_ts ON agent_activity(activity_type, ts DESC);
|
||||
CREATE INDEX idx_agent_activity_entity ON agent_activity(entity_id, ts DESC);
|
||||
CREATE INDEX idx_agent_activity_correlation ON agent_activity(correlation_id);
|
||||
DO $$ BEGIN
|
||||
PERFORM add_retention_policy('agent_activity', INTERVAL '90 days');
|
||||
EXCEPTION WHEN OTHERS THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- ─── Ledger view (R3-11: not a fourth write path) ────────────────────
|
||||
CREATE VIEW ledger AS
|
||||
SELECT e.created_at AS ts, e.entity_id AS execution_id, e.target_entity_id,
|
||||
e.action, e.risk_class, e.status, e.verified,
|
||||
c.route, c.reasoning, a.status AS approval_status, a.decided_by,
|
||||
e.agent_id, e.correlation_id
|
||||
FROM executions e
|
||||
LEFT JOIN classifications c ON c.entity_id = e.classification_id
|
||||
LEFT JOIN approvals a ON a.entity_id = e.approval_id;
|
||||
7
migrations/embed.go
Normal file
7
migrations/embed.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// Package migrations embeds SQL migration files for use by the db package.
|
||||
package migrations
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.up.sql
|
||||
var FS embed.FS
|
||||
Reference in New Issue
Block a user