feat: Phase 9 gaps closed — ApprovalService.Decide convergence, execlog fold, execworker poller
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

- ApprovalService (core/app/approval.go) + ApprovalRepo (postgres adapter) with
  full decide transaction: HMAC token verify, approval flip, execution un-gate,
  session-scoped window keys (+session suffix matching GovernanceStore gate),
  nomos session flip, audit+event on failure abort. httpapi DecideApproval now
  a thin presenter delegating to the service. ListPending payload format fixed
  (json.Unmarshal not raw-wrap).
- execlog folded into postgres adapter: internal/execlog deleted, NewExecutionLog
  / ReadExecutionLog live in the db package, callers updated (mcp, httpapi).
- execworker poller over ExecutionService.DispatchQueued: advisory lock leak
  fixed (defer/recover per execution), correlation_id preserved via Finalize
  event emission (ExecRunRepo.Finalize now emits execution.{status} with
  correlation_id from the row).
- Phase 8 session export-rename completed: Store, New, and all 53 methods
  exported; cmd/nomos/ agent.go fixed to use session.PendingContinuation etc.
- Coverage gates: ExecutionService.Submit 93.1%, PolicyService.Decide 100%.
- Plans index updated, VERSION bumped to 0.36.0.
This commit is contained in:
2026-08-16 12:29:59 +02:00
parent 986937799a
commit b98d7c24bf
27 changed files with 1161 additions and 600 deletions

View File

@@ -24,8 +24,8 @@ import (
"time"
"github.com/dtoro/oikos/internal/actuator"
"github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/core/app"
"github.com/dtoro/oikos/internal/core/ports"
"github.com/dtoro/oikos/internal/httpapi/gen"
@@ -65,10 +65,10 @@ type Server struct {
// entities is the entity-aggregate use-case service (Phase 3 of the
// hexagonal refactor); entityRepo exposes the idempotency reads the
// replay path needs. Wired here until main becomes the composition root.
entities *app.EntityService
entityRepo *db.EntityRepo
readModels ports.ReadModels
relService *app.RelationshipService
entities *app.EntityService
entityRepo *db.EntityRepo
readModels ports.ReadModels
relService *app.RelationshipService
// provisioning owns the pct_create flow (Phase 7): spec defaults,
// provisioner dispatch, guest registration in the graph.
provisioning *app.ProvisioningService
@@ -77,6 +77,10 @@ type Server struct {
// execSvc is the run-submission use-case (Phase 4): PolicyService
// gating + auto-run/queue dispatch, shared by the MCP tools.
execSvc *app.ExecutionService
// approvalSvc is the approval-decision use-case (Phase 9): token
// verification + execution un-gating, shared by the REST decision
// endpoint and the MCP decide_approval path.
approvalSvc *app.ApprovalService
}
// NewHandler builds the full HTTP handler: /healthz (unauthenticated,
@@ -86,7 +90,7 @@ type Server struct {
// holds a dedicated pooled connection for LISTEN. Callers MUST cancel ctx
// before closing the pool — otherwise the held connection never releases
// and pool.Close() deadlocks.
func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config, entities *app.EntityService, entityRepo *db.EntityRepo, readModels ports.ReadModels, relService *app.RelationshipService, provisioning *app.ProvisioningService, seeds *app.SeedService, execSvc *app.ExecutionService) http.Handler {
func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config, entities *app.EntityService, entityRepo *db.EntityRepo, readModels ports.ReadModels, relService *app.RelationshipService, provisioning *app.ProvisioningService, seeds *app.SeedService, execSvc *app.ExecutionService, approvalSvc *app.ApprovalService) http.Handler {
s := &Server{
pool: pool,
cfg: cfg,
@@ -100,6 +104,7 @@ func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config, entities
provisioning: provisioning,
seeds: seeds,
execSvc: execSvc,
approvalSvc: approvalSvc,
}
// Wire secrets backend: Infisical primary with SOPS DR fallback.
@@ -948,10 +953,10 @@ main();
// ListenAndServe runs the API server with graceful shutdown on ctx cancel
// (SG4): stop accepting, drain in-flight for up to 30s, then exit.
func ListenAndServe(ctx context.Context, pool *db.Pool, cfg config.Config, entities *app.EntityService, entityRepo *db.EntityRepo, readModels ports.ReadModels, relService *app.RelationshipService, provisioning *app.ProvisioningService, seeds *app.SeedService, execSvc *app.ExecutionService) error {
func ListenAndServe(ctx context.Context, pool *db.Pool, cfg config.Config, entities *app.EntityService, entityRepo *db.EntityRepo, readModels ports.ReadModels, relService *app.RelationshipService, provisioning *app.ProvisioningService, seeds *app.SeedService, execSvc *app.ExecutionService, approvalSvc *app.ApprovalService) error {
srv := &http.Server{
Addr: cfg.APIListen,
Handler: NewHandler(ctx, pool, cfg, entities, entityRepo, readModels, relService, provisioning, seeds, execSvc),
Handler: NewHandler(ctx, pool, cfg, entities, entityRepo, readModels, relService, provisioning, seeds, execSvc, approvalSvc),
ReadHeaderTimeout: 10 * time.Second,
}