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

@@ -1,7 +1,6 @@
package main
import (
"time"
"context"
"fmt"
"log/slog"
@@ -10,19 +9,20 @@ import (
"os/signal"
"strings"
"syscall"
"time"
"github.com/google/uuid"
"github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/adapters/remote"
"github.com/dtoro/oikos/internal/adapters/ssh"
"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/adapters/postgres"
"github.com/dtoro/oikos/internal/adapters/remote"
intremote "github.com/dtoro/oikos/internal/remote"
"github.com/dtoro/oikos/internal/adapters/ssh"
"github.com/dtoro/oikos/internal/execworker"
"github.com/dtoro/oikos/internal/httpapi"
"github.com/dtoro/oikos/internal/observability"
intremote "github.com/dtoro/oikos/internal/remote"
"github.com/dtoro/oikos/internal/scheduler"
"github.com/dtoro/oikos/internal/secrets"
)
@@ -59,9 +59,9 @@ func main() {
)
if sec != nil {
overlays := secrets.ConfigOverlays(map[string]func(string){
"mcp_bearer-token": func(v string) { cfg.MCPBearerToken = v },
"api_token": func(v string) { cfg.APIToken = v },
"oidc_client-secret": func(v string) { cfg.OIDCClientSecret = v },
"mcp_bearer-token": func(v string) { cfg.MCPBearerToken = v },
"api_token": func(v string) { cfg.APIToken = v },
"oidc_client-secret": func(v string) { cfg.OIDCClientSecret = v },
})
n := secrets.OverlayConfig(ctx, sec, overlays)
slog.Info("secrets resolved from Infisical", "count", n)
@@ -117,7 +117,7 @@ func main() {
slog.Info("all: starting api with scheduler + execution-worker in background")
svc := buildAPIServices(pool)
if err := httpapi.ListenAndServe(ctx, pool, cfg, svc.entities, svc.entityRepo, svc.readModels, svc.relService, svc.provisioning, svc.seeds, svc.execSvc); err != nil {
if err := httpapi.ListenAndServe(ctx, pool, cfg, svc.entities, svc.entityRepo, svc.readModels, svc.relService, svc.provisioning, svc.seeds, svc.execSvc, svc.approvalSvc); err != nil {
slog.Error("api failed", "error", err)
os.Exit(1)
}
@@ -210,7 +210,7 @@ func runAPI(ctx context.Context, cfg config.Config) error {
}
svc := buildAPIServices(pool)
err = httpapi.ListenAndServe(ctx, pool, cfg, svc.entities, svc.entityRepo, svc.readModels, svc.relService, svc.provisioning, svc.seeds, svc.execSvc)
err = httpapi.ListenAndServe(ctx, pool, cfg, svc.entities, svc.entityRepo, svc.readModels, svc.relService, svc.provisioning, svc.seeds, svc.execSvc, svc.approvalSvc)
if err == http.ErrServerClosed {
return nil
}
@@ -228,6 +228,7 @@ type apiServices struct {
provisioning *app.ProvisioningService
seeds *app.SeedService
execSvc *app.ExecutionService
approvalSvc *app.ApprovalService
}
func buildAPIServices(pool *db.Pool) apiServices {
@@ -256,6 +257,7 @@ func buildAPIServices(pool *db.Pool) apiServices {
return intremote.ResolveProxmoxHostSlug(ctx, pool, id, "")
}
execSvc := app.NewExecutionService(policySvc, executor, resolver, db.NewExecRunRepo(pool))
approvalSvc := app.NewApprovalService(db.NewApprovalRepo(pool))
return apiServices{
entities: entities,
@@ -265,6 +267,7 @@ func buildAPIServices(pool *db.Pool) apiServices {
provisioning: provisioning,
seeds: seeds,
execSvc: execSvc,
approvalSvc: approvalSvc,
}
}
@@ -394,9 +397,9 @@ func runSecretAudit(ctx context.Context, cfg config.Config) {
backend := newInfisicalBackendOrFail(cfg)
envValues := map[string]string{
"mcp_bearer-token": cfg.MCPBearerToken,
"api_token": cfg.APIToken,
"oidc_client-secret": cfg.OIDCClientSecret,
"mcp_bearer-token": cfg.MCPBearerToken,
"api_token": cfg.APIToken,
"oidc_client-secret": cfg.OIDCClientSecret,
}
fmt.Println("key infisical env status")
@@ -525,4 +528,4 @@ func runExport(ctx context.Context, cfg config.Config) error {
slog.Info("exported", "file", path, "bytes", len(content))
}
return nil
}
}