feat: Phase 2 — ports package, secrets port move, postgres adapter move
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Problem: the hexagon's Phase 2 (plans/2026-08-15-hexagonal-architecture.md)
must give the use-cases-to-be their contract surface: driven-port
interfaces, test fakes, the secrets interface moved into core, and the
postgres package inside the adapters tree — before the first vertical
slice (Phase 3) can wire a composition root.

Change:
- internal/core/ports: full driven-port catalog per plan §3.3 —
  repositories as transaction-scoped aggregates whose inputs carry
  derived checks, audit, and events (§3.6), plus CommandExecutor,
  TargetResolver, Checker, Secrets, EventPublisher, Provisioner.
  Port-local payload types (Event, AuditEntry, CheckDef, KnowledgeEntry,
  ExecResult) keep signatures off infrastructure; TypeTree aliases
  internal/ontology (pure over domain) until checkdefaults is absorbed.
  ReadModels intentionally not declared yet — it materializes with the
  Phase 3 slice and grows as report handlers rewire.
- secrets.Backend is now an alias of ports.Secrets; implementations
  (Infisical, SOPS, Manager) unchanged. mcp's local secretBackend
  subset is deleted; tool constructors take ports.Secrets.
- internal/db → internal/adapters/postgres (mechanical import rewrite;
  package identifier stays db until the Phase 3 repository split).
  sqlc.yaml, Makefile, golangci exclusions, and docs follow the move;
  make generate-check verified.
- internal/adapters/ssh: Executor implements ports.CommandExecutor over
  the actuator dial pool + RunStreaming (10-min default timeout carried
  over from the httpapi path).
- internal/adapters/remote: Resolver implements ports.TargetResolver
  delegating to internal/remote (still pool-based; drops onto
  ports.EntityRepository when repositories land in Phase 3 — documented
  transitional import).
- internal/core/ports/portstest: importable fakes — in-memory
  EntityRepo (with check-then-act SetState, side-effect recording),
  RecordingExecutor, FakeChecker, SpyPublisher; port-satisfaction
  guards; tests.

Risk: ports are declared ahead of implementations — signatures firm up
per phase as slices land (documented in the package doc); the
remote→postgres transitional import is explicit and dissolves in
Phase 3.

Verification: go vet, make test (race, 19 packages), generate-check,
golangci on core+adapters — 0 issues; full-repo baseline down
365→344.
This commit is contained in:
2026-08-15 22:56:56 +02:00
parent d4d99a7473
commit 64f7d54011
93 changed files with 1102 additions and 103 deletions

View File

@@ -14,9 +14,9 @@ cmd/webhook/main.go Gitea deploy-webhook receiver (push-to-deploy on mac-
internal/httpapi/ REST + MCP server. Chi router. OpenAPI-generated types from internal/httpapi/ REST + MCP server. Chi router. OpenAPI-generated types from
internal/httpapi/gen/api.gen.go. Strict server in impl.go. internal/httpapi/gen/api.gen.go. Strict server in impl.go.
internal/mcp/ MCP tool implementations (get_entity, search_knowledge, etc.) internal/mcp/ MCP tool implementations (get_entity, search_knowledge, etc.)
internal/db/ Connection pool (pool.go), seed ingestion (seed.go), DB→YAML internal/adapters/postgres/ Connection pool (pool.go), seed ingestion (seed.go), DB→YAML
export (export.go), type hierarchy (typetree.go) export (export.go), type hierarchy (typetree.go)
internal/db/queries/ SQL query files → sqlc generates internal/db/sqlcgen/ internal/adapters/postgres/queries/ SQL query files → sqlc generates sqlcgen/ (same dir)
internal/scheduler/ Observe loop: probes, signals, check_defs internal/scheduler/ Observe loop: probes, signals, check_defs
internal/actuator/ SSH execution with circuit breaker + retry internal/actuator/ SSH execution with circuit breaker + retry
internal/learning/ Pattern extraction, anomaly detection internal/learning/ Pattern extraction, anomaly detection
@@ -91,8 +91,8 @@ current phase status). To add a new capability:
## SQL conventions ## SQL conventions
- Queries live in `internal/db/queries/*.sql` with `-- name: FuncName :exec` - Queries live in `internal/adapters/postgres/queries/*.sql` with `-- name: FuncName :exec`
annotations for sqlc. Generated code in `internal/db/sqlcgen/` — never annotations for sqlc. Generated code in `internal/adapters/postgres/sqlcgen/` — never
hand-edit. Call via `sqlcgen.New(pool).QueryName(ctx, params)`. hand-edit. Call via `sqlcgen.New(pool).QueryName(ctx, params)`.
- **sqlc is the default** for all DB access. Raw `pool.Query/Exec` with inline - **sqlc is the default** for all DB access. Raw `pool.Query/Exec` with inline
SQL is a documented carve-out for cases sqlc can't express: `LISTEN`/`NOTIFY`, SQL is a documented carve-out for cases sqlc can't express: `LISTEN`/`NOTIFY`,

View File

@@ -72,7 +72,7 @@ linters:
path: internal/httpapi/gen/ path: internal/httpapi/gen/
- linters: - linters:
- all - all
path: internal/db/sqlcgen/ path: internal/adapters/postgres/sqlcgen/
paths: paths:
- third_party$ - third_party$
- builtin$ - builtin$

View File

@@ -60,7 +60,9 @@ internal/ All Go packages
here phase by phase here phase by phase
httpapi/ REST + MCP server (OpenAPI-generated) httpapi/ REST + MCP server (OpenAPI-generated)
mcp/ MCP tool implementations mcp/ MCP tool implementations
db/ Connection pool, migrations, seeds, sqlc queries db/ (moved) → internal/adapters/postgres: pool, migrations,
seeds, sqlc queries — package still named `db` until
the Phase 3 repository split
scheduler/ Observe loop, probes, signals scheduler/ Observe loop, probes, signals
actuator/ SSH execution actuator/ SSH execution
learning/ Pattern recognition, anomaly detection learning/ Pattern recognition, anomaly detection
@@ -116,8 +118,8 @@ Never hand-edit `internal/httpapi/gen/api.gen.go`.
### Database access is sqlc-first ### Database access is sqlc-first
SQL queries live in `internal/db/queries/*.sql`. Go code is generated with SQL queries live in `internal/adapters/postgres/queries/*.sql`. Go code is generated with
`sqlc` into `internal/db/sqlcgen/`. Config in `sqlc.yaml`. `sqlc` into `internal/adapters/postgres/sqlcgen/`. Config in `sqlc.yaml`.
- Queries target pgx/v5 with UUID + timestamptz overrides - Queries target pgx/v5 with UUID + timestamptz overrides
- Never hand-edit generated sqlc code - Never hand-edit generated sqlc code

View File

@@ -18,7 +18,7 @@ test-db:
docker compose up -d postgres docker compose up -d postgres
@sleep 3 @sleep 3
OIKOS_TEST_DATABASE_URL="postgres://oikos:$${OIKOS_DB_PASSWORD:-oikos_dev}@localhost:5432/oikos?sslmode=disable" \ OIKOS_TEST_DATABASE_URL="postgres://oikos:$${OIKOS_DB_PASSWORD:-oikos_dev}@localhost:5432/oikos?sslmode=disable" \
$(GO) test -race -count=1 ./internal/db/ ./internal/httpapi/ ./internal/mcp/ $(GO) test -race -count=1 ./internal/adapters/postgres/ ./internal/httpapi/ ./internal/mcp/
lint: vet golangci govulncheck lint: vet golangci govulncheck
@@ -40,7 +40,7 @@ generate:
# CI drift guard: regenerate and fail if the committed output changed. # CI drift guard: regenerate and fail if the committed output changed.
generate-check: generate generate-check: generate
@git diff --exit-code -- internal/httpapi/gen internal/db/sqlcgen \ @git diff --exit-code -- internal/httpapi/gen internal/adapters/postgres/sqlcgen \
|| (echo "generated code is stale — run 'make generate' and commit" && exit 1) || (echo "generated code is stale — run 'make generate' and commit" && exit 1)
migrate: migrate:

View File

@@ -1 +1 @@
0.32.2 0.32.3

View File

@@ -10,7 +10,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"

View File

@@ -17,7 +17,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )

View File

@@ -11,7 +11,7 @@ import (
"syscall" "syscall"
"github.com/dtoro/oikos/internal/config" "github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/execworker" "github.com/dtoro/oikos/internal/execworker"
"github.com/dtoro/oikos/internal/httpapi" "github.com/dtoro/oikos/internal/httpapi"
"github.com/dtoro/oikos/internal/knowledge" "github.com/dtoro/oikos/internal/knowledge"

View File

@@ -14,8 +14,8 @@ import (
"time" "time"
"github.com/dtoro/oikos/internal/config" "github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/google/uuid" "github.com/google/uuid"
) )

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"log/slog" "log/slog"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/secrets" "github.com/dtoro/oikos/internal/secrets"
) )

View File

@@ -0,0 +1,6 @@
// Package db is the postgres adapter: connection pool, migrations, seed
// ingest, and sqlc-generated queries. It moved from internal/db in Phase 2
// of the hexagonal refactor (ADR 0016); the package identifier stays `db`
// until the repository split (Phase 3) renames it alongside the first
// ports implementations landing here.
package db

View File

@@ -12,12 +12,15 @@ type entityCacheEntry struct {
exp time.Time exp time.Time
} }
// EntityCache is a TTL cache mapping entity IDs to slugs and back,
// keyed for the hot resolution paths.
type EntityCache struct { type EntityCache struct {
mu sync.RWMutex mu sync.RWMutex
m map[string]entityCacheEntry m map[string]entityCacheEntry
ttl time.Duration ttl time.Duration
} }
// NewEntityCache builds a cache with the given TTL.
func NewEntityCache(ttl time.Duration) *EntityCache { func NewEntityCache(ttl time.Duration) *EntityCache {
return &EntityCache{ return &EntityCache{
m: make(map[string]entityCacheEntry), m: make(map[string]entityCacheEntry),
@@ -25,6 +28,7 @@ func NewEntityCache(ttl time.Duration) *EntityCache {
} }
} }
// GetSlug resolves an entity ID to its slug.
func (c *EntityCache) GetSlug(id string) (string, bool) { func (c *EntityCache) GetSlug(id string) (string, bool) {
c.mu.RLock() c.mu.RLock()
e, ok := c.m[id] e, ok := c.m[id]
@@ -35,6 +39,7 @@ func (c *EntityCache) GetSlug(id string) (string, bool) {
return e.slug, true return e.slug, true
} }
// GetID resolves a slug to its entity ID.
func (c *EntityCache) GetID(slug string) (string, bool) { func (c *EntityCache) GetID(slug string) (string, bool) {
c.mu.RLock() c.mu.RLock()
e, ok := c.m[slug] e, ok := c.m[slug]
@@ -45,6 +50,7 @@ func (c *EntityCache) GetID(slug string) (string, bool) {
return e.id, true return e.id, true
} }
// Set records the slug/id pair and serialized attributes.
func (c *EntityCache) Set(slug, id, attrs string) { func (c *EntityCache) Set(slug, id, attrs string) {
exp := time.Now().Add(c.ttl) exp := time.Now().Add(c.ttl)
c.mu.Lock() c.mu.Lock()
@@ -53,6 +59,7 @@ func (c *EntityCache) Set(slug, id, attrs string) {
c.mu.Unlock() c.mu.Unlock()
} }
// Invalidate drops the cached entries for one slug/id pair.
func (c *EntityCache) Invalidate(slug, id string) { func (c *EntityCache) Invalidate(slug, id string) {
c.mu.Lock() c.mu.Lock()
delete(c.m, slug) delete(c.m, slug)

View File

@@ -6,7 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )

View File

@@ -56,7 +56,11 @@ func (p *Pool) Migrate(ctx context.Context) error {
if _, err := conn.Exec(ctx, "SELECT pg_advisory_lock($1)", migrationLockKey); err != nil { if _, err := conn.Exec(ctx, "SELECT pg_advisory_lock($1)", migrationLockKey); err != nil {
return fmt.Errorf("acquire migration lock: %w", err) return fmt.Errorf("acquire migration lock: %w", err)
} }
defer conn.Exec(context.WithoutCancel(ctx), "SELECT pg_advisory_unlock($1)", migrationLockKey) defer func() {
if _, err := conn.Exec(context.WithoutCancel(ctx), "SELECT pg_advisory_unlock($1)", migrationLockKey); err != nil {
slog.Warn("postgres: release migration lock failed", "error", err)
}
}()
// Create tracking table if not exists // Create tracking table if not exists
_, err = conn.Exec(ctx, ` _, err = conn.Exec(ctx, `
@@ -156,7 +160,11 @@ func (p *Pool) SeedIngest(ctx context.Context, filename string, content []byte,
if err != nil { if err != nil {
return fmt.Errorf("begin tx: %w", err) return fmt.Errorf("begin tx: %w", err)
} }
defer tx.Rollback(ctx) defer func() {
if err := tx.Rollback(ctx); err != nil {
slog.Debug("postgres: rollback after failed ingest", "error", err)
}
}()
if err := ingestFn(ctx, tx, data); err != nil { if err := ingestFn(ctx, tx, data); err != nil {
return fmt.Errorf("ingest %s: %w", filename, err) return fmt.Errorf("ingest %s: %w", filename, err)

View File

@@ -0,0 +1,63 @@
// Package remote implements ports.TargetResolver over internal/remote.
// The resolver logic (address preference, guest wrapping, hosting-compute
// walks) is unchanged; this adapter maps its results onto the port types.
// When the postgres repositories land (Phase 3+), the underlying functions
// move into this package on top of ports.EntityRepository.
package remote
import (
"context"
"github.com/google/uuid"
postgres "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/core/ports"
intremote "github.com/dtoro/oikos/internal/remote"
)
// Resolver resolves execution targets from the entity graph.
type Resolver struct {
pool *postgres.Pool
}
var _ ports.TargetResolver = (*Resolver)(nil)
// NewResolver builds a resolver over the postgres pool.
func NewResolver(pool *postgres.Pool) *Resolver { return &Resolver{pool: pool} }
func toPort(t intremote.ExecTarget) ports.Target {
return ports.Target{Host: t.Host, User: t.User, Wrap: t.Wrap}
}
// ResolveExecTarget resolves a slug to its execution endpoint.
func (r *Resolver) ResolveExecTarget(ctx context.Context, targetSlug string) (ports.Target, error) {
t, err := intremote.ResolveExecTarget(ctx, r.pool, targetSlug, intremote.DefaultUser)
if err != nil {
return ports.Target{}, err
}
return toPort(t), nil
}
// ResolveForCheck resolves a check's entity (by ID and type) to its endpoint.
func (r *Resolver) ResolveForCheck(ctx context.Context, targetID domain.UUID, targetType string) (ports.Target, error) {
id, err := uuid.Parse(string(targetID))
if err != nil {
return ports.Target{}, err
}
t, err := intremote.ResolveExecTargetForCheck(ctx, r.pool, id, targetType, intremote.DefaultUser)
if err != nil {
return ports.Target{}, err
}
return toPort(t), nil
}
// ResolveHost resolves a host slug to address and SSH user.
func (r *Resolver) ResolveHost(ctx context.Context, hostSlug, fallbackUser string) (string, string, error) {
return intremote.ResolveHost(ctx, r.pool, hostSlug, fallbackUser)
}
// IsGuest reports whether an entity type is reached via pct/qm exec.
func (r *Resolver) IsGuest(entityType string) bool {
return intremote.IsGuest(entityType)
}

View File

@@ -0,0 +1,114 @@
// Package ssh implements ports.CommandExecutor over internal/actuator:
// the dial pool, host-key handling, and streaming/combined execution.
package ssh
import (
"context"
"fmt"
"log/slog"
"os"
"sync"
"time"
cryptossh "golang.org/x/crypto/ssh"
"github.com/dtoro/oikos/internal/actuator"
"github.com/dtoro/oikos/internal/core/ports"
)
const (
defaultExecTimeout = 10 * time.Minute
defaultKeyPathEnv = "OIKOS_SSH_KEY_PATH"
defaultKeyPath = "/etc/oikos/ssh_key"
)
// SignerSource supplies the SSH signer used for all dials. The secrets
// adapter provides one backed by Infisical/SOPS; tests inject a static one.
type SignerSource func(ctx context.Context) (cryptossh.Signer, error)
// FileSignerSource reads an OpenSSH private key from disk once and parses
// it (path from env OIKOS_SSH_KEY_PATH, default /etc/oikos/ssh_key — the
// same resolution the httpapi path used before the extraction).
func FileSignerSource() SignerSource {
var (
once sync.Once
signer cryptossh.Signer
err error
)
return func(context.Context) (cryptossh.Signer, error) {
once.Do(func() {
path := os.Getenv(defaultKeyPathEnv)
if path == "" {
path = defaultKeyPath
}
key, rerr := os.ReadFile(path)
if rerr != nil {
err = fmt.Errorf("read ssh key %s: %w", path, rerr)
return
}
signer, err = actuator.LoadSignerFromBytes(key)
})
return signer, err
}
}
// Executor runs commands over SSH through a dial pool.
type Executor struct {
signer SignerSource
pool *actuator.DialPool
}
var _ ports.CommandExecutor = (*Executor)(nil)
// NewExecutor builds an executor. The dial pool reuses connections per
// host/user for the given TTL.
func NewExecutor(signer SignerSource, poolTTL time.Duration) *Executor {
return &Executor{
signer: signer,
pool: actuator.NewDialPool(poolTTL),
}
}
// Close releases pooled connections.
func (e *Executor) Close() { e.pool.Close() }
// Run dials the target (via the pool), wraps the command for transport when
// the target needs it (pct/qm guests), executes with streaming output, and
// maps the outcome onto ports.ExecResult.
func (e *Executor) Run(ctx context.Context, target ports.Target, command string, opts ports.ExecOpts) ports.ExecResult {
start := time.Now()
signer, err := e.signer(ctx)
if err != nil {
return ports.ExecResult{Err: err, Duration: time.Since(start)}
}
client, err := e.pool.Get(ctx, actuator.DialOptions{
Host: target.Host,
User: target.User,
Signer: signer,
})
if err != nil {
return ports.ExecResult{Err: err, Duration: time.Since(start)}
}
// Pooled client: do not close here; the pool evicts on TTL.
if target.Wrap != nil {
command = target.Wrap(command)
}
timeout := opts.Timeout
if timeout <= 0 {
timeout = defaultExecTimeout
}
output, runErr := actuator.RunStreaming(ctx, client, command, opts.Sink, timeout)
if runErr != nil {
slog.Debug("ssh exec: command failed", "host", target.Host, "error", runErr)
}
return ports.ExecResult{
Output: output,
Duration: time.Since(start),
Err: runErr,
}
}

View File

@@ -13,7 +13,7 @@ package audit
import ( import (
"context" "context"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
) )
// Finding is one drift item the operator should look at. // Finding is one drift item the operator should look at.

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"testing" "testing"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid" "github.com/google/uuid"
) )

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )

View File

@@ -2,4 +2,16 @@
// repositories, executors, resolvers, probes, secrets, and events. // repositories, executors, resolvers, probes, secrets, and events.
// Core packages define these interfaces; adapters under internal/adapters // Core packages define these interfaces; adapters under internal/adapters
// implement them. See docs/adr/0016-hexagonal-ports-adapters.md. // implement them. See docs/adr/0016-hexagonal-ports-adapters.md.
//
// Port rules (plan §3.3/§3.6):
// - Command-side repository methods are transaction-scoped aggregates:
// one method = one BEGIN…COMMIT = everything that must succeed or
// fail together. Input structs carry derived checks, audit entries,
// and events as fields.
// - Read methods are plain queries.
// - ReadModels (query-shaped report reads consumed directly by the
// httpapi/mcpserver adapters) materializes with the first vertical
// slice in Phase 3 and grows as report handlers rewire.
// - Signatures reference core/domain types (plus port-local payload
// types); they firm up per phase as the slices land.
package ports package ports

View File

@@ -0,0 +1,83 @@
package ports
import (
"context"
"github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/ontology"
)
// TypeTree aliases the ontology tree: entity types, relationship types,
// lifecycle definitions. ontology is pure over domain; it moves under
// core/ when checkdefaults is absorbed (Phase 3).
type TypeTree = ontology.TypeTree
// EntityFilters bounds entity list/search reads.
type EntityFilters struct {
Type string
State string
Q string
Limit int
}
// EntityCreateInput is one transaction: the entity, its derived check
// definitions, and the audit/event side-effects of the creation.
type EntityCreateInput struct {
Entity domain.Entity
DerivedChecks []CheckDef
Audit []AuditEntry
Event *Event
}
// EntityUpdateInput mirrors EntityCreateInput for updates.
type EntityUpdateInput struct {
Entity domain.Entity
DerivedChecks []CheckDef
Audit []AuditEntry
Event *Event
}
// EntityTransitionInput is a lifecycle state change: the check-then-act
// precondition (current state) is validated inside the transaction.
type EntityTransitionInput struct {
Slug string
From string
To string
Audit []AuditEntry
Event *Event
}
// EntityRepository is the entity aggregate. Command methods are
// transaction-scoped: everything in the input commits or nothing does.
type EntityRepository interface {
Get(ctx context.Context, id domain.UUID) (domain.Entity, error)
BySlug(ctx context.Context, slug string) (domain.Entity, error)
List(ctx context.Context, filters EntityFilters) ([]domain.Entity, error)
Search(ctx context.Context, q string, limit int) ([]domain.Entity, error)
Create(ctx context.Context, input EntityCreateInput) (domain.Entity, error)
Update(ctx context.Context, input EntityUpdateInput) (domain.Entity, error)
SetState(ctx context.Context, input EntityTransitionInput) (domain.Entity, error)
}
// RelationshipCreateInput validates endpoints against the ontology in core
// before this is called; the repository persists the edge (+audit/event).
type RelationshipCreateInput struct {
Relationship domain.Relationship
Audit []AuditEntry
Event *Event
}
// RelationshipRepository is the relationship aggregate.
type RelationshipRepository interface {
Create(ctx context.Context, input RelationshipCreateInput) (domain.Relationship, error)
End(ctx context.Context, source, target domain.UUID, relType string) error
ListFor(ctx context.Context, entityID domain.UUID, direction string) ([]domain.Relationship, error)
}
// OntologyStore loads the type tree; implementations cache. Consumers
// validate entity types, relationship endpoints, and lifecycle transitions
// against it before issuing repository writes.
type OntologyStore interface {
LoadTypeTree(ctx context.Context) (TypeTree, error)
}

View File

@@ -0,0 +1,45 @@
package ports
import (
"context"
"time"
"github.com/dtoro/oikos/internal/core/domain"
)
// Event is a domain occurrence worth publishing (SSE + events table). The
// events adapter maps it to the persistence/SSE shapes.
type Event struct {
Type string // e.g. "execution.completed", "health.changed"
Severity string // "info", "warning", "critical"
Source string // "api", "mcp", "scheduler", "webhook"
EntityID domain.UUID
Data map[string]any
Ts time.Time
}
// EventPublisher fans events out to subscribers and persists them. The
// events adapter owns the SSE broker, the events table, and the dedicated
// LISTEN/NOTIFY connection.
type EventPublisher interface {
Publish(ctx context.Context, event Event) error
}
// AuditEntry is an append-only audit-log record.
type AuditEntry struct {
ActorType string // "agent", "operator", "system"
ActorLabel string
Action string
EntityID domain.UUID
Details map[string]any
Ts time.Time
}
// AuditRepository appends audit records and events. Entries are usually
// carried inside other repositories' input structs so they commit in the
// same transaction (§3.6 of the plan); the standalone methods serve
// read-path actions that audit without another aggregate write.
type AuditRepository interface {
AppendAudit(ctx context.Context, entries []AuditEntry) error
AppendEvent(ctx context.Context, events []Event) error
}

View File

@@ -0,0 +1,74 @@
package ports
import (
"context"
"time"
"github.com/dtoro/oikos/internal/core/domain"
)
// Target is a resolved execution endpoint (mirrors remote.ExecTarget; the
// remote adapter maps between them). Wrap rewrites a plain command for
// transport: identity for a host, pct/qm wrapping for guests.
type Target struct {
Host string
User string
Wrap func(cmd string) string
}
// ExecOpts carries execution options; Sink, when non-nil, receives output
// chunks as they arrive (streaming path).
type ExecOpts struct {
Timeout time.Duration
Sink func(stream string, chunk []byte)
}
// ExecResult is the outcome of one command execution.
type ExecResult struct {
Output string
Duration time.Duration
Err error
}
// CommandExecutor runs a command on a resolved target over SSH. Implemented
// by the ssh adapter wrapping internal/actuator (dial pool + breaker).
type CommandExecutor interface {
Run(ctx context.Context, target Target, command string, opts ExecOpts) ExecResult
}
// TargetResolver resolves entity slugs/IDs to execution endpoints.
// Implemented by the remote adapter on top of EntityRepository.
type TargetResolver interface {
ResolveExecTarget(ctx context.Context, targetSlug string) (Target, error)
ResolveForCheck(ctx context.Context, targetID domain.UUID, targetType string) (Target, error)
ResolveHost(ctx context.Context, hostSlug, fallbackUser string) (addr, user string, err error)
// IsGuest reports whether an entity type is reached via pct/qm exec
// through a Proxmox host rather than by direct SSH.
IsGuest(entityType string) bool
}
// Provisioner creates guests via pct/qm on a Proxmox host (Phase 7 fills
// the input payloads in; signatures firm up with ProvisioningService).
type Provisioner interface {
CreateLXC(ctx context.Context, host domain.UUID, input LXCInput) (domain.UUID, error)
CreateVM(ctx context.Context, host domain.UUID, input VMInput) (domain.UUID, error)
}
// LXCInput is a placeholder until ProvisioningService (Phase 7) fixes the
// create payloads; declared now so the port surface is complete.
type LXCInput struct {
Name string
Template string
Cores int
MemoryMB int
DiskGB int
}
// VMInput mirrors LXCInput for VM creation via qm.
type VMInput struct {
Name string
TemplateID int
Cores int
MemoryMB int
DiskGB int
}

View File

@@ -0,0 +1,97 @@
package ports
import (
"context"
"time"
"github.com/dtoro/oikos/internal/core/domain"
)
// SignalUpsertInput is the observe-pass signal write: open/resolve
// transitions with their triggers, committed atomically.
type SignalUpsertInput struct {
Signal domain.Signal
Triggers []SignalTrigger
Audit []AuditEntry
Event *Event
}
// SignalTrigger is an automatic follow-up fired on a signal transition
// (payload firm; shapes firm up with the Phase 5 observation slice).
type SignalTrigger struct {
Kind string
EntityID domain.UUID
Parameters map[string]any
}
// SignalTransitionInput acks/resolves/mutes a signal (check-then-act on the
// signal's current state inside the transaction).
type SignalTransitionInput struct {
SignalID domain.UUID
Action string // "ack", "resolve", "mute"
Note string
MuteFor time.Duration
Actor string
Audit []AuditEntry
Event *Event
}
// SignalRepository is the signal aggregate.
type SignalRepository interface {
Open(ctx context.Context) ([]domain.Signal, error)
History(ctx context.Context, entityID domain.UUID, limit int) ([]domain.Signal, error)
UpsertWithTriggers(ctx context.Context, input SignalUpsertInput) error
Transition(ctx context.Context, input SignalTransitionInput) (domain.Signal, error)
}
// ExecutionSubmitInput is the queued-execution write: execution row,
// approval (for gated risk classes), audit, and event — one transaction.
type ExecutionSubmitInput struct {
Execution domain.Execution
Approval *domain.Approval
Audit []AuditEntry
Event *Event
}
// ExecutionCompleteInput closes out an execution: final status, output
// summary, audit, event.
type ExecutionCompleteInput struct {
ExecutionID domain.UUID
Status string
Output string
ExitCode int
Audit []AuditEntry
Event *Event
}
// ExecutionRepository is the execution aggregate. Claim uses an advisory
// lock so exactly one worker claims a queued execution.
type ExecutionRepository interface {
List(ctx context.Context, cursor string, limit int) ([]domain.Execution, error)
ReadLog(ctx context.Context, executionID domain.UUID) ([]string, error)
SubmitQueued(ctx context.Context, input ExecutionSubmitInput) (domain.Execution, error)
Claim(ctx context.Context) (*domain.Execution, error)
AppendLog(ctx context.Context, executionID domain.UUID, chunk string) error
Complete(ctx context.Context, input ExecutionCompleteInput) error
}
// ApprovalDecideInput verifies the HMAC token (check-then-act), flips the
// approval, un-gates the execution, and appends audit — one transaction.
// Double-approve must not double-execute.
type ApprovalDecideInput struct {
ApprovalID domain.UUID
Token string
Approved bool
Actor string
Audit []AuditEntry
Event *Event
}
// ApprovalRepository is the approval aggregate.
type ApprovalRepository interface {
ListPending(ctx context.Context, entityID domain.UUID, limit int) ([]domain.Approval, error)
Decide(ctx context.Context, input ApprovalDecideInput) (domain.Approval, error)
}

View File

@@ -0,0 +1,54 @@
package ports
import (
"context"
"time"
"github.com/dtoro/oikos/internal/core/domain"
)
// KnowledgeEntry is a knowledge-base document/investigation/runbook.
type KnowledgeEntry struct {
Slug string
Title string
Kind string // "document", "investigation", "runbook"
Tags []string
Content string
About []string // entity slugs the entry describes
UpdatedAt time.Time
}
// KnowledgeUpsertInput is the knowledge write: row + revision + about-edges
// in one transaction.
type KnowledgeUpsertInput struct {
Entry KnowledgeEntry
Audit []AuditEntry
Event *Event
}
// KnowledgeRepository is the knowledge aggregate.
type KnowledgeRepository interface {
Search(ctx context.Context, query string, limit int) ([]KnowledgeEntry, error)
GetContent(ctx context.Context, slug string) (KnowledgeEntry, error)
Revisions(ctx context.Context, slug string, limit int) ([]KnowledgeEntry, error)
Tags(ctx context.Context) (map[string]int, error)
Orphans(ctx context.Context, staleDays int) ([]KnowledgeEntry, error)
Duplicates(ctx context.Context, threshold float64) ([]KnowledgeEntry, error)
Upsert(ctx context.Context, input KnowledgeUpsertInput) (KnowledgeEntry, error)
Merge(ctx context.Context, targetSlug string, sourceSlugs []string) error
SoftDelete(ctx context.Context, slug string) error
Restore(ctx context.Context, slug string) error
}
// LearningRepository — patterns, feedback, skills (reads plus simple
// upserts; no multi-aggregate transactions here).
type LearningRepository interface {
ListFeedback(ctx context.Context, limit int) ([]domain.Pattern, error)
ListPatterns(ctx context.Context, limit int) ([]domain.Pattern, error)
ListSkills(ctx context.Context) ([]domain.Pattern, error)
UpsertPattern(ctx context.Context, pattern domain.Pattern) error
Validate(ctx context.Context, patternID domain.UUID) error
Quarantine(ctx context.Context, patternID domain.UUID, reason string) error
}

View File

@@ -0,0 +1,61 @@
package ports
import (
"context"
"encoding/json"
"time"
"github.com/dtoro/oikos/internal/core/domain"
)
// CheckDef is a health-check definition (subset of the check_defs row the
// observe pass consumes; grows with the probe adapters in Phase 5).
type CheckDef struct {
ID domain.UUID
EntityID domain.UUID
Kind string
Name string
Config json.RawMessage
IntervalS int
Enabled bool
Severity string
CreatedAt time.Time
UpdatedAt time.Time
}
// CheckResult is what one probe run produces.
type CheckResult struct {
Value float64
State string // "ok", "warning", "critical", "unknown"
Message string
}
// Checker probes one check kind. One adapter per kind under adapters/probes;
// the ObservationService picks the adapter by CheckDef.Kind.
type Checker interface {
Check(ctx context.Context, def CheckDef, target Target) CheckResult
}
// MetricSample is one metric observation to record.
type MetricSample struct {
Metric string
Value float64
Timestamp time.Time
}
// MetricsRepository records observe-pass metrics (Timescale write path;
// bucketed/trend reads belong to ReadModels).
type MetricsRepository interface {
InsertSamples(ctx context.Context, entityID domain.UUID, samples []MetricSample) error
}
// CheckRepository — monitoring definitions. EnsureFor is the read-diff-write
// derivation in one transaction.
type CheckRepository interface {
ListEnabled(ctx context.Context) ([]CheckDef, error)
ListFor(ctx context.Context, entityID domain.UUID) ([]CheckDef, error)
// EnsureFor inserts defs that are missing, updates changed ones, and
// removes stale ones for the entity — atomically.
EnsureFor(ctx context.Context, entityID domain.UUID, desired []CheckDef) error
SetEnabled(ctx context.Context, checkID domain.UUID, enabled bool) error
}

View File

@@ -0,0 +1,242 @@
// Package portstest provides in-memory fakes for the driven ports, for
// use in core/app service tests (and anywhere else a backend-free stand-in
// helps).
package portstest
import (
"context"
"fmt"
"strings"
"sync"
"github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/core/ports"
)
// EntityRepo is an in-memory ports.EntityRepository. Command inputs' audit,
// event, and derived-check fields are recorded for assertion.
type EntityRepo struct {
mu sync.Mutex
byID map[domain.UUID]domain.Entity
bySlug map[string]domain.UUID
order []domain.UUID
nextID int
Audits []ports.AuditEntry
Events []ports.Event
Checks map[domain.UUID][]ports.CheckDef
ErrStub error // returned by every command when set
}
// NewEntityRepo builds an empty in-memory entity repository.
func NewEntityRepo() *EntityRepo {
return &EntityRepo{
byID: make(map[domain.UUID]domain.Entity),
bySlug: make(map[string]domain.UUID),
Checks: make(map[domain.UUID][]ports.CheckDef),
}
}
// Get returns the entity by ID.
func (r *EntityRepo) Get(_ context.Context, id domain.UUID) (domain.Entity, error) {
r.mu.Lock()
defer r.mu.Unlock()
e, ok := r.byID[id]
if !ok {
return domain.Entity{}, domain.ErrNotFound
}
return e, nil
}
// BySlug returns the entity by slug.
func (r *EntityRepo) BySlug(_ context.Context, slug string) (domain.Entity, error) {
r.mu.Lock()
defer r.mu.Unlock()
id, ok := r.bySlug[slug]
if !ok {
return domain.Entity{}, domain.ErrNotFound
}
return r.byID[id], nil
}
// List returns entities filtered by type/state, bounded by limit.
func (r *EntityRepo) List(_ context.Context, f ports.EntityFilters) ([]domain.Entity, error) {
r.mu.Lock()
defer r.mu.Unlock()
var out []domain.Entity
for _, id := range r.order {
e := r.byID[id]
if f.Type != "" && e.Type != f.Type {
continue
}
if f.State != "" && e.State != f.State {
continue
}
out = append(out, e)
if f.Limit > 0 && len(out) >= f.Limit {
break
}
}
return out, nil
}
// Search matches name/slug substrings.
func (r *EntityRepo) Search(ctx context.Context, q string, limit int) ([]domain.Entity, error) {
// Substring over name/slug is enough for service tests.
all, err := r.List(ctx, ports.EntityFilters{Limit: limit})
if err != nil {
return nil, err
}
var out []domain.Entity
for _, e := range all {
if strings.Contains(e.Name, q) || strings.Contains(e.Slug, q) {
out = append(out, e)
}
}
return out, nil
}
// Create stores a new entity and records its input side-effects.
func (r *EntityRepo) Create(_ context.Context, in ports.EntityCreateInput) (domain.Entity, error) {
if r.ErrStub != nil {
return domain.Entity{}, r.ErrStub
}
r.mu.Lock()
defer r.mu.Unlock()
if in.Entity.ID == "" {
r.nextID++
in.Entity.ID = domain.UUID(fmt.Sprintf("fake-entity-%03d", r.nextID))
}
if _, dup := r.bySlug[in.Entity.Slug]; dup {
return domain.Entity{}, domain.ErrConflict
}
r.store(in.Entity)
r.Audits = append(r.Audits, in.Audit...)
if in.Event != nil {
r.Events = append(r.Events, *in.Event)
}
r.Checks[in.Entity.ID] = in.DerivedChecks
return in.Entity, nil
}
// Update replaces a stored entity and records its input side-effects.
func (r *EntityRepo) Update(_ context.Context, in ports.EntityUpdateInput) (domain.Entity, error) {
if r.ErrStub != nil {
return domain.Entity{}, r.ErrStub
}
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.byID[in.Entity.ID]; !ok {
return domain.Entity{}, domain.ErrNotFound
}
delete(r.bySlug, r.byID[in.Entity.ID].Slug)
r.store(in.Entity)
r.Audits = append(r.Audits, in.Audit...)
if in.Event != nil {
r.Events = append(r.Events, *in.Event)
}
r.Checks[in.Entity.ID] = in.DerivedChecks
return in.Entity, nil
}
// SetState applies a lifecycle transition, refusing stale From states.
func (r *EntityRepo) SetState(_ context.Context, in ports.EntityTransitionInput) (domain.Entity, error) {
if r.ErrStub != nil {
return domain.Entity{}, r.ErrStub
}
r.mu.Lock()
defer r.mu.Unlock()
id, ok := r.bySlug[in.Slug]
if !ok {
return domain.Entity{}, domain.ErrNotFound
}
e := r.byID[id]
if e.State != in.From {
return domain.Entity{}, domain.ErrConflict
}
e.State = in.To
r.byID[id] = e
r.Audits = append(r.Audits, in.Audit...)
if in.Event != nil {
r.Events = append(r.Events, *in.Event)
}
return e, nil
}
func (r *EntityRepo) store(e domain.Entity) {
r.byID[e.ID] = e
r.bySlug[e.Slug] = e.ID
r.order = append(r.order, e.ID)
}
// FindBySlug is a test helper bypassing the port interface.
func (r *EntityRepo) FindBySlug(slug string) (domain.Entity, bool) {
r.mu.Lock()
defer r.mu.Unlock()
id, ok := r.bySlug[slug]
if !ok {
return domain.Entity{}, false
}
return r.byID[id], true
}
// RecordingExecutor records every command it is asked to run and replies
// with canned results (default: empty success).
type RecordingExecutor struct {
mu sync.Mutex
Calls []ExecCall
Results []ports.ExecResult // popped in order; last one repeats
}
// ExecCall is one recorded CommandExecutor.Run invocation.
type ExecCall struct {
Target ports.Target
Command string
}
// Run records the call and replies with the next canned result.
func (e *RecordingExecutor) Run(_ context.Context, target ports.Target, command string, _ ports.ExecOpts) ports.ExecResult {
e.mu.Lock()
defer e.mu.Unlock()
e.Calls = append(e.Calls, ExecCall{Target: target, Command: command})
if len(e.Results) == 0 {
return ports.ExecResult{}
}
res := e.Results[0]
if len(e.Results) > 1 {
e.Results = e.Results[1:]
}
return res
}
// FakeChecker answers per check kind; unregistered kinds report "unknown".
type FakeChecker struct {
mu sync.Mutex
ByKind map[string]ports.CheckResult
Calls []string
}
// Check answers from the kind map, recording the call.
func (c *FakeChecker) Check(_ context.Context, def ports.CheckDef, _ ports.Target) ports.CheckResult {
c.mu.Lock()
defer c.mu.Unlock()
c.Calls = append(c.Calls, def.Kind)
if res, ok := c.ByKind[def.Kind]; ok {
return res
}
return ports.CheckResult{State: "unknown"}
}
// SpyPublisher records published events.
type SpyPublisher struct {
mu sync.Mutex
Events []ports.Event
Err error
}
// Publish records the event.
func (p *SpyPublisher) Publish(_ context.Context, event ports.Event) error {
p.mu.Lock()
defer p.mu.Unlock()
p.Events = append(p.Events, event)
return p.Err
}

View File

@@ -0,0 +1,121 @@
package portstest
import (
"context"
"errors"
"testing"
"github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/core/ports"
)
func TestEntityRepoCreateReadUpdate(t *testing.T) {
r := NewEntityRepo()
ctx := context.Background()
e, err := r.Create(ctx, ports.EntityCreateInput{
Entity: domain.Entity{Slug: "lxc:test", Name: "test", Type: "lxc", State: "active"},
Audit: []ports.AuditEntry{{Action: "entity.create"}},
Event: &ports.Event{Type: "entity.created"},
})
if err != nil {
t.Fatalf("create: %v", err)
}
if e.ID == "" {
t.Fatal("create did not assign an ID")
}
got, err := r.BySlug(ctx, "lxc:test")
if err != nil {
t.Fatalf("bySlug: %v", err)
}
if got.ID != e.ID {
t.Fatalf("bySlug returned %s, want %s", got.ID, e.ID)
}
if len(r.Audits) != 1 || r.Audits[0].Action != "entity.create" {
t.Fatalf("audit not recorded: %+v", r.Audits)
}
if len(r.Events) != 1 || r.Events[0].Type != "entity.created" {
t.Fatalf("event not recorded: %+v", r.Events)
}
_, err = r.Create(ctx, ports.EntityCreateInput{
Entity: domain.Entity{Slug: "lxc:test", Name: "dup", Type: "lxc"},
})
if !errors.Is(err, domain.ErrConflict) && !errors.Is(err, domain.ErrAlreadyExists) {
t.Fatalf("duplicate slug: got %v, want conflict", err)
}
}
func TestEntityRepoSetStateCheckThenAct(t *testing.T) {
r := NewEntityRepo()
ctx := context.Background()
_, err := r.Create(ctx, ports.EntityCreateInput{
Entity: domain.Entity{Slug: "host:one", Name: "one", Type: "host", State: "active"},
})
if err != nil {
t.Fatalf("create: %v", err)
}
_, err = r.SetState(ctx, ports.EntityTransitionInput{Slug: "host:one", From: "planned", To: "decommissioned"})
if !errors.Is(err, domain.ErrConflict) {
t.Fatalf("stale From: got %v, want ErrConflict", err)
}
e, err := r.SetState(ctx, ports.EntityTransitionInput{Slug: "host:one", From: "active", To: "decommissioned"})
if err != nil {
t.Fatalf("SetState: %v", err)
}
if e.State != "decommissioned" {
t.Fatalf("state = %s, want decommissioned", e.State)
}
}
func TestRecordingExecutor(t *testing.T) {
e := &RecordingExecutor{Results: []ports.ExecResult{{Output: "first"}, {Output: "second"}}}
res := e.Run(context.Background(), ports.Target{Host: "h", User: "root"}, "uptime", ports.ExecOpts{})
if res.Output != "first" {
t.Fatalf("first run output = %q", res.Output)
}
e.Run(context.Background(), ports.Target{Host: "h"}, "w", ports.ExecOpts{})
e.Run(context.Background(), ports.Target{Host: "h"}, "true", ports.ExecOpts{})
if len(e.Calls) != 3 || e.Calls[2].Command != "true" {
t.Fatalf("calls not recorded: %+v", e.Calls)
}
if got := e.Run(context.Background(), ports.Target{}, "x", ports.ExecOpts{}); got.Output != "second" {
t.Fatalf("last result should repeat, got %q", got.Output)
}
}
func TestFakeChecker(t *testing.T) {
c := &FakeChecker{ByKind: map[string]ports.CheckResult{
"http": {State: "ok", Value: 200},
}}
res := c.Check(context.Background(), ports.CheckDef{Kind: "http"}, ports.Target{})
if res.State != "ok" || res.Value != 200 {
t.Fatalf("http check = %+v", res)
}
res = c.Check(context.Background(), ports.CheckDef{Kind: "dns"}, ports.Target{})
if res.State != "unknown" {
t.Fatalf("unregistered kind = %+v, want unknown", res)
}
}
func TestSpyPublisher(t *testing.T) {
p := &SpyPublisher{}
if err := p.Publish(context.Background(), ports.Event{Type: "x"}); err != nil {
t.Fatalf("publish: %v", err)
}
if len(p.Events) != 1 || p.Events[0].Type != "x" {
t.Fatalf("events = %+v", p.Events)
}
}
// Interface satisfaction guards: the fakes must implement the ports.
var (
_ ports.EntityRepository = (*EntityRepo)(nil)
_ ports.CommandExecutor = (*RecordingExecutor)(nil)
_ ports.Checker = (*FakeChecker)(nil)
_ ports.EventPublisher = (*SpyPublisher)(nil)
)

View File

@@ -0,0 +1,14 @@
package ports
import "context"
// Secrets retrieves and stores secrets. Infisical/SOPS implementations
// stay in internal/secrets; the caching Manager satisfies this by
// delegation. The interface lives here so core code never imports the
// backends.
type Secrets interface {
Get(ctx context.Context, key string) (string, error)
List(ctx context.Context) ([]string, error)
Set(ctx context.Context, key string, value string) error
Name() string
}

View File

@@ -14,8 +14,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"
"github.com/google/uuid" "github.com/google/uuid"
) )

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"github.com/dtoro/oikos/internal/config" "github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
) )
// RunnerForMain provides the run function for registration in main. // RunnerForMain provides the run function for registration in main.

View File

@@ -14,8 +14,8 @@ import (
"github.com/dtoro/oikos/internal/actuator" "github.com/dtoro/oikos/internal/actuator"
"github.com/dtoro/oikos/internal/config" "github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/health" "github.com/dtoro/oikos/internal/health"
"github.com/dtoro/oikos/internal/remote" "github.com/dtoro/oikos/internal/remote"
"github.com/google/uuid" "github.com/google/uuid"

View File

@@ -12,8 +12,8 @@ import (
"time" "time"
"github.com/dtoro/oikos/internal/actuator" "github.com/dtoro/oikos/internal/actuator"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/execlog" "github.com/dtoro/oikos/internal/execlog"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"
"github.com/google/uuid" "github.com/google/uuid"

View File

@@ -16,7 +16,7 @@ import (
"testing" "testing"
"github.com/dtoro/oikos/internal/config" "github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -7,7 +7,7 @@ import (
"log/slog" "log/slog"
"time" "time"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -9,7 +9,7 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -3,7 +3,7 @@ package httpapi
import ( import (
"context" "context"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )

View File

@@ -6,7 +6,7 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/google/uuid" "github.com/google/uuid"
) )

View File

@@ -9,8 +9,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -7,7 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"time" "time"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
) )

View File

@@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/google/uuid" "github.com/google/uuid"

View File

@@ -5,7 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
) )

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -7,7 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -25,7 +25,7 @@ import (
"github.com/dtoro/oikos/internal/actuator" "github.com/dtoro/oikos/internal/actuator"
"github.com/dtoro/oikos/internal/config" "github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
mcphandler "github.com/dtoro/oikos/internal/mcp" mcphandler "github.com/dtoro/oikos/internal/mcp"
"github.com/dtoro/oikos/internal/safego" "github.com/dtoro/oikos/internal/safego"

View File

@@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"

View File

@@ -11,7 +11,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/httpapi/gen" "github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"

View File

@@ -11,8 +11,8 @@ import (
"time" "time"
"github.com/dtoro/oikos/internal/config" "github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/google/uuid" "github.com/google/uuid"
) )

View File

@@ -4,7 +4,7 @@ import (
"math" "math"
"testing" "testing"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
) )
func TestWilsonLowerBound(t *testing.T) { func TestWilsonLowerBound(t *testing.T) {

View File

@@ -4,13 +4,14 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/core/ports"
"github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/policy" "github.com/dtoro/oikos/internal/policy"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
func AnalysisTools(pool *db.Pool, agentID uuid.UUID, sec secretBackend) []toolReg { func AnalysisTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
return []toolReg{ return []toolReg{
{tool: &mcp.Tool{Name: "get_health_summary", Description: "Fleet health per entity — optionally filter by health state(s)", {tool: &mcp.Tool{Name: "get_health_summary", Description: "Fleet health per entity — optionally filter by health state(s)",
InputSchema: objSchema( InputSchema: objSchema(

View File

@@ -16,7 +16,7 @@ import (
"testing" "testing"
"github.com/dtoro/oikos/internal/checkdefaults" "github.com/dtoro/oikos/internal/checkdefaults"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"

View File

@@ -5,7 +5,7 @@ import (
"context" "context"
"strings" "strings"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/execlog" "github.com/dtoro/oikos/internal/execlog"
"github.com/dtoro/oikos/internal/remote" "github.com/dtoro/oikos/internal/remote"
) )

View File

@@ -7,13 +7,14 @@ import (
"strings" "strings"
"github.com/dtoro/oikos/internal/audit" "github.com/dtoro/oikos/internal/audit"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/core/ports"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
func EntityTools(pool *db.Pool, agentID uuid.UUID, sec secretBackend) []toolReg { func EntityTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
return []toolReg{ return []toolReg{
{tool: &mcp.Tool{Name: "ping", Description: "Lightweight connectivity check. Returns server identity, no DB hit.", {tool: &mcp.Tool{Name: "ping", Description: "Lightweight connectivity check. Returns server identity, no DB hit.",
InputSchema: objSchema(), InputSchema: objSchema(),

View File

@@ -6,12 +6,13 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/core/ports"
"github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
func KnowledgeTools(pool *db.Pool, agentID uuid.UUID, sec secretBackend) []toolReg { func KnowledgeTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
return []toolReg{ return []toolReg{
{tool: &mcp.Tool{Name: "search_knowledge", Description: "Full-text search across documentation (PostgreSQL FTS with ts_rank ranking). Returns a short snippet per hit, not the full note — call get_knowledge_content with the returned slug to read the whole thing.", {tool: &mcp.Tool{Name: "search_knowledge", Description: "Full-text search across documentation (PostgreSQL FTS with ts_rank ranking). Returns a short snippet per hit, not the full note — call get_knowledge_content with the returned slug to read the whole thing.",
InputSchema: objSchema(prop{"query", "string", "Search terms"}), InputSchema: objSchema(prop{"query", "string", "Search terms"}),

View File

@@ -10,14 +10,15 @@ import (
"strings" "strings"
"time" "time"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/core/ports"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
func OpsTools(pool *db.Pool, agentID uuid.UUID, sec secretBackend) []toolReg { func OpsTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
return []toolReg{ return []toolReg{
// ── request_execution (legacy fixed enum) retired 2026-07-14 ── // ── request_execution (legacy fixed enum) retired 2026-07-14 ──
// All mutations now route through `run`. The handler functions // All mutations now route through `run`. The handler functions

View File

@@ -20,8 +20,9 @@ import (
"time" "time"
"github.com/dtoro/oikos/internal/actuator" "github.com/dtoro/oikos/internal/actuator"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/core/ports"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/execlog" "github.com/dtoro/oikos/internal/execlog"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"
"github.com/dtoro/oikos/internal/policy" "github.com/dtoro/oikos/internal/policy"
@@ -48,18 +49,9 @@ func objSchema(props ...prop) *jsonschema.Schema {
return s return s
} }
// secretBackend is the interface MCP tools use to access the secrets store.
// Defined here to avoid importing the full secrets package (which brings in
// the Infisical SDK). Mirrors the subset of secrets.Backend used by tools.
type secretBackend interface {
Get(ctx context.Context, key string) (string, error)
Set(ctx context.Context, key string, value string) error
List(ctx context.Context) ([]string, error)
}
// NewHandler creates an http.Handler that serves the Oikos MCP server. // NewHandler creates an http.Handler that serves the Oikos MCP server.
// agentID is the Nomos agent entity UUID; tool calls are logged to agent_activity. // agentID is the Nomos agent entity UUID; tool calls are logged to agent_activity.
func NewHandler(pool *db.Pool, token string, agentID uuid.UUID, sec secretBackend) http.Handler { func NewHandler(pool *db.Pool, token string, agentID uuid.UUID, sec ports.Secrets) http.Handler {
s := newServer(pool, agentID, sec) s := newServer(pool, agentID, sec)
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
if token != "" { if token != "" {
@@ -75,7 +67,7 @@ func NewHandler(pool *db.Pool, token string, agentID uuid.UUID, sec secretBacken
// toolHandler is the function signature registered via AddTool. // toolHandler is the function signature registered via AddTool.
type toolHandler = mcp.ToolHandler type toolHandler = mcp.ToolHandler
func newServer(pool *db.Pool, agentID uuid.UUID, sec secretBackend) *mcp.Server { func newServer(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) *mcp.Server {
s := mcp.NewServer(&mcp.Implementation{Name: "oikos", Version: "dev"}, &mcp.ServerOptions{ s := mcp.NewServer(&mcp.Implementation{Name: "oikos", Version: "dev"}, &mcp.ServerOptions{
Logger: slog.Default(), Logger: slog.Default(),
}) })

View File

@@ -6,7 +6,8 @@ import (
"strings" "strings"
"github.com/dtoro/oikos/internal/checkdefaults" "github.com/dtoro/oikos/internal/checkdefaults"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/core/ports"
"github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
@@ -16,7 +17,7 @@ type toolReg struct {
handler toolHandler handler toolHandler
} }
func allTools(pool *db.Pool, agentID uuid.UUID, sec secretBackend) []toolReg { func allTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
return append(append(append(append( return append(append(append(append(
[]toolReg{}, []toolReg{},
EntityTools(pool, agentID, sec)...), EntityTools(pool, agentID, sec)...),

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/google/uuid" "github.com/google/uuid"
) )

View File

@@ -22,7 +22,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid" "github.com/google/uuid"
) )

View File

@@ -6,7 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid" "github.com/google/uuid"
) )

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
) )
// checkBackupFreshness reports whether a backup target has a recent artifact. // checkBackupFreshness reports whether a backup target has a recent artifact.

View File

@@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
) )
func backupCheckDef(t *testing.T, config string) sqlcgen.ListEnabledCheckDefsRow { func backupCheckDef(t *testing.T, config string) sqlcgen.ListEnabledCheckDefsRow {

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid" "github.com/google/uuid"
) )

View File

@@ -12,7 +12,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"github.com/dtoro/oikos/internal/config" "github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
) )
// RunnerForMain provides the run function for registration in main. // RunnerForMain provides the run function for registration in main.

View File

@@ -21,8 +21,8 @@ import (
"github.com/dtoro/oikos/internal/actuator" "github.com/dtoro/oikos/internal/actuator"
"github.com/dtoro/oikos/internal/config" "github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/db" "github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
"github.com/dtoro/oikos/internal/health" "github.com/dtoro/oikos/internal/health"
"github.com/dtoro/oikos/internal/observability" "github.com/dtoro/oikos/internal/observability"
"github.com/dtoro/oikos/internal/remote" "github.com/dtoro/oikos/internal/remote"

View File

@@ -6,18 +6,17 @@ import (
"log/slog" "log/slog"
"sync" "sync"
"time" "time"
"github.com/dtoro/oikos/internal/core/ports"
) )
var ErrNotFound = errors.New("secret not found") var ErrNotFound = errors.New("secret not found")
var ErrBackendUnavailable = errors.New("secret backend unavailable") var ErrBackendUnavailable = errors.New("secret backend unavailable")
// Backend is the interface for retrieving and storing secrets. // Backend is the secrets port. The interface lives in core/ports (ADR 0016);
type Backend interface { // this alias keeps existing call sites working while implementations
Get(ctx context.Context, key string) (string, error) // (Infisical, SOPS, Manager) stay in this package.
List(ctx context.Context) ([]string, error) type Backend = ports.Secrets
Set(ctx context.Context, key string, value string) error
Name() string
}
// Manager holds a primary and fallback backend. If the primary fails, // Manager holds a primary and fallback backend. If the primary fails,
// it falls back to the secondary. Supports periodic background refresh // it falls back to the secondary. Supports periodic background refresh

View File

@@ -1,7 +1,7 @@
# Hexagonal architecture for Oikos — design and phased refactor plan # Hexagonal architecture for Oikos — design and phased refactor plan
**Date:** 2026-08-15 **Date:** 2026-08-15
**Status:** In progress — Phase 0 shipped; Phases 19 pending **Status:** In progress — Phases 02 shipped; Phases 39 pending
**Scope:** All Go code (`cmd/oikos`, `cmd/nomos`, `cmd/webhook`) and the UI **Scope:** All Go code (`cmd/oikos`, `cmd/nomos`, `cmd/webhook`) and the UI
split. One hexagon covers the oikos backend; nomos is an external agent split. One hexagon covers the oikos backend; nomos is an external agent
client that gets an internal cleanup (Phase 8) but stays outside the core. client that gets an internal cleanup (Phase 8) but stays outside the core.

View File

@@ -22,7 +22,7 @@ went sideways, open an investigation.
| 2026-08-04 | [Hermes MCP client integration](done/2026-08-04-hermes-mcp-client-integration.md) | Done — deployed | | 2026-08-04 | [Hermes MCP client integration](done/2026-08-04-hermes-mcp-client-integration.md) | Done — deployed |
| 2026-08-05 | [Agent execution safety: QEMU guest agent gate + host-mutation guard](done/2026-08-05-agent-execution-safety-qemu-guest-agent-gate.md) | Done — implemented (1b9c761) | | 2026-08-05 | [Agent execution safety: QEMU guest agent gate + host-mutation guard](done/2026-08-05-agent-execution-safety-qemu-guest-agent-gate.md) | Done — implemented (1b9c761) |
| 2026-08-05 | [Backend evaluation: architecture, security, and reliability improvements](2026-08-05-backend-evaluation-improvements.md) | Done — all three phases (B, D, E) implemented as code (0.28.00.29.0), deployed, and hardened via review. Remaining: C (security) and F (performance) backlog. | | 2026-08-05 | [Backend evaluation: architecture, security, and reliability improvements](2026-08-05-backend-evaluation-improvements.md) | Done — all three phases (B, D, E) implemented as code (0.28.00.29.0), deployed, and hardened via review. Remaining: C (security) and F (performance) backlog. |
| 2026-08-15 | [Hexagonal architecture — design and phased refactor](2026-08-15-hexagonal-architecture.md) | In Progress — Phase 0 done (ADR 0016, core scaffold, domain moved, depguard) | | 2026-08-15 | [Hexagonal architecture — design and phased refactor](2026-08-15-hexagonal-architecture.md) | In Progress — Phases 02 done (ADR 0016, client extracted to dtoro/oikos-web, ports + adapters scaffold); Phase 3 next |
## Done ## Done

View File

@@ -1,19 +1,19 @@
# sqlc — type-safe Go from SQL (plan SG17). `make generate` regenerates. # sqlc — type-safe Go from SQL (plan SG17). `make generate` regenerates.
# #
# Scope: API read/mutation paths use sqlc-generated queries # Scope: API read/mutation paths use sqlc-generated queries
# (internal/db/sqlcgen). The seed ingest and YAML export intentionally stay # (internal/adapters/postgres/sqlcgen). The seed ingest and YAML export intentionally stay
# hand-written pgx: they are generic bulk upserts driven by parsed YAML # hand-written pgx: they are generic bulk upserts driven by parsed YAML
# shapes, where sqlc's static typing adds nothing. # shapes, where sqlc's static typing adds nothing.
version: "2" version: "2"
sql: sql:
- engine: "postgresql" - engine: "postgresql"
schema: "migrations" schema: "migrations"
queries: "internal/db/queries" queries: "internal/adapters/postgres/queries"
strict_order_by: false strict_order_by: false
gen: gen:
go: go:
package: "sqlcgen" package: "sqlcgen"
out: "internal/db/sqlcgen" out: "internal/adapters/postgres/sqlcgen"
sql_package: "pgx/v5" sql_package: "pgx/v5"
emit_pointers_for_null_types: true emit_pointers_for_null_types: true
overrides: overrides: