refactor: split phase3.go + extract MCP tool registry (R4)
internal/httpapi/phase3.go (2627 lines, 12+ resource domains) split into 15 per-resource files: - actuator.go: SSH execution machinery (initSSH, sshExec, resolveRunTarget, executeApprovedAction, jsonErr, gatewayPreflightPassed, resolveTemplate) - checks.go, classifications.go, executions.go, approvals.go, patterns.go, skills.go, approval_rules.go, autonomy.go, risk_classes.go, relationships.go, entity_types.go, metrics.go, agent_activity.go, helpers.go — one file per resource domain, each with its own imports. internal/mcp/server.go: newServer (708 lines, 33 inline tool registrations) refactored to a registry pattern: - internal/mcp/tools.go (new): toolReg struct + allTools() returning all 33 tool definitions. Handler logic moved verbatim — no changes to tool names, descriptions, schemas, or behavior. - server.go: newServer is now 9 lines (iterate registry, AddTool each). -699 lines. No function logic, names, or signatures changed. go vet, build, and all tests pass (httpapi, mcp, db, policy).
This commit is contained in:
304
internal/httpapi/checks.go
Normal file
304
internal/httpapi/checks.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dtoro/oikos/internal/db/sqlcgen"
|
||||
"github.com/dtoro/oikos/internal/domain"
|
||||
"github.com/dtoro/oikos/internal/httpapi/gen"
|
||||
"github.com/dtoro/oikos/internal/observability"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// ─── Checks ────────────────────────────────────────────────────────────
|
||||
|
||||
func (s *Server) ListChecks(ctx context.Context, req gen.ListChecksRequestObject) (gen.ListChecksResponseObject, error) {
|
||||
limit := clampLimit(req.Params.Limit)
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT cd.entity_id, e.slug, cd.kind,
|
||||
COALESCE(te.slug, '') AS target_slug, cd.target_type,
|
||||
cd.config, cd.interval_s, cd.timeout_s, cd.zone, cd.enabled,
|
||||
e.version
|
||||
FROM check_defs cd
|
||||
JOIN entities e ON e.id = cd.entity_id
|
||||
LEFT JOIN entities te ON te.id = cd.target_id
|
||||
WHERE ($1::text IS NULL OR cd.kind = $1)
|
||||
AND ($2::text IS NULL OR te.slug = $2)
|
||||
AND ($3::bool IS NULL OR cd.enabled = $3)
|
||||
AND ($4::text IS NULL OR e.slug > $4)
|
||||
ORDER BY e.slug
|
||||
LIMIT $5`,
|
||||
req.Params.Kind, req.Params.Target, req.Params.Enabled, req.Params.Cursor, limit+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []gen.Check{}
|
||||
for rows.Next() {
|
||||
var c gen.Check
|
||||
var targetSlug string
|
||||
var configBytes []byte
|
||||
if err := rows.Scan(&c.Id, &c.Slug, &c.Kind, &targetSlug, &c.TargetType,
|
||||
&configBytes, &c.IntervalS, &c.TimeoutS, &c.Zone, &c.Enabled, &c.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if targetSlug != "" {
|
||||
c.Target = &targetSlug
|
||||
}
|
||||
var config map[string]any
|
||||
if len(configBytes) > 0 && json.Unmarshal(configBytes, &config) == nil && len(config) > 0 {
|
||||
c.Config = &config
|
||||
}
|
||||
items = append(items, c)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, rows.Err()
|
||||
}
|
||||
|
||||
var next *string
|
||||
if len(items) > limit {
|
||||
items = items[:limit]
|
||||
next = &items[len(items)-1].Slug
|
||||
}
|
||||
if items == nil {
|
||||
items = []gen.Check{}
|
||||
}
|
||||
return gen.ListChecks200JSONResponse{Items: items, NextCursor: next}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateCheck(ctx context.Context, req gen.CreateCheckRequestObject) (gen.CreateCheckResponseObject, error) {
|
||||
if req.Body == nil {
|
||||
return nil, fmt.Errorf("%w: request body is required", domain.ErrInvalidInput)
|
||||
}
|
||||
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slug := req.Body.Slug
|
||||
if slug == "" {
|
||||
slug = "check:" + string(req.Body.Kind) + ":" + uuid.New().String()[:8]
|
||||
}
|
||||
|
||||
// Resolve target if provided.
|
||||
var targetID *uuid.UUID
|
||||
if req.Body.Target != nil && *req.Body.Target != "" {
|
||||
tid, rerr := s.resolveEntityID(ctx, *req.Body.Target)
|
||||
if rerr != nil {
|
||||
return nil, rerr
|
||||
}
|
||||
targetID = &tid
|
||||
}
|
||||
|
||||
intervalS := int32(300)
|
||||
if req.Body.IntervalS != nil {
|
||||
intervalS = int32(*req.Body.IntervalS)
|
||||
}
|
||||
timeoutS := int32(30)
|
||||
if req.Body.TimeoutS != nil {
|
||||
timeoutS = int32(*req.Body.TimeoutS)
|
||||
}
|
||||
enabled := true
|
||||
if req.Body.Enabled != nil {
|
||||
enabled = *req.Body.Enabled
|
||||
}
|
||||
|
||||
configJSON := []byte("{}")
|
||||
if req.Body.Config != nil {
|
||||
configJSON, _ = json.Marshal(req.Body.Config)
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
q := sqlcgen.New(tx)
|
||||
|
||||
// Create the entity row (checks are entities).
|
||||
entity, err := q.InsertEntity(ctx, sqlcgen.InsertEntityParams{
|
||||
ID: id,
|
||||
Slug: slug,
|
||||
Type: "check",
|
||||
Name: slug,
|
||||
Attributes: []byte("{}"),
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "duplicate") {
|
||||
return nil, fmt.Errorf("%w: check %q already exists", domain.ErrAlreadyExists, slug)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := q.InsertCheckDef(ctx, sqlcgen.InsertCheckDefParams{
|
||||
EntityID: id,
|
||||
TargetID: targetID,
|
||||
TargetType: req.Body.TargetType,
|
||||
Kind: string(req.Body.Kind),
|
||||
Config: configJSON,
|
||||
IntervalS: intervalS,
|
||||
TimeoutS: timeoutS,
|
||||
Zone: req.Body.Zone,
|
||||
Enabled: enabled,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build response Check.
|
||||
check := gen.Check{
|
||||
Id: id,
|
||||
Slug: entity.Slug,
|
||||
Kind: gen.CheckKind(req.Body.Kind),
|
||||
IntervalS: int(intervalS),
|
||||
TimeoutS: int(timeoutS),
|
||||
Enabled: enabled,
|
||||
TargetType: req.Body.TargetType,
|
||||
Zone: req.Body.Zone,
|
||||
Version: int(entity.Version),
|
||||
}
|
||||
if req.Body.Config != nil {
|
||||
check.Config = req.Body.Config
|
||||
}
|
||||
if targetID != nil && req.Body.Target != nil {
|
||||
check.Target = req.Body.Target
|
||||
}
|
||||
|
||||
actorType, actor := actorInfo(ctx)
|
||||
if auditErr := observability.Audit(ctx, q, actorType, actor, "create",
|
||||
&id, "POST", "/api/v1/checks", "",
|
||||
map[string]any{"kind": req.Body.Kind, "slug": slug}); auditErr != nil {
|
||||
return nil, auditErr
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gen.CreateCheck201JSONResponse(check), nil
|
||||
}
|
||||
|
||||
func (s *Server) PatchCheck(ctx context.Context, req gen.PatchCheckRequestObject) (gen.PatchCheckResponseObject, error) {
|
||||
if req.Body == nil {
|
||||
return nil, fmt.Errorf("%w: request body is required", domain.ErrInvalidInput)
|
||||
}
|
||||
|
||||
id, err := s.resolveEntityID(ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse If-Match
|
||||
ifMatch := strings.Trim(req.Params.IfMatch, `"`)
|
||||
expectedVersion, err := parseIntIfMatch(ifMatch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = expectedVersion // check_defs don't track version via If-Match today, but we validate the header is present
|
||||
|
||||
if ifMatch == "" {
|
||||
return nil, fmt.Errorf("%w: invalid If-Match header", domain.ErrInvalidInput)
|
||||
}
|
||||
|
||||
// Get current check def
|
||||
current, err := sqlcgen.New(s.pool).GetCheckDef(ctx, id)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, fmt.Errorf("%w: check %s", domain.ErrNotFound, req.Id)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
// Apply patch.
|
||||
if req.Body.Config != nil {
|
||||
current.Config, _ = json.Marshal(req.Body.Config)
|
||||
}
|
||||
if req.Body.IntervalS != nil {
|
||||
current.IntervalS = int32(*req.Body.IntervalS)
|
||||
}
|
||||
if req.Body.TimeoutS != nil {
|
||||
current.TimeoutS = int32(*req.Body.TimeoutS)
|
||||
}
|
||||
if req.Body.Enabled != nil {
|
||||
current.Enabled = *req.Body.Enabled
|
||||
}
|
||||
|
||||
if err := sqlcgen.New(tx).UpdateCheckDef(ctx, sqlcgen.UpdateCheckDefParams{
|
||||
EntityID: id,
|
||||
Kind: current.Kind,
|
||||
Config: current.Config,
|
||||
IntervalS: current.IntervalS,
|
||||
TimeoutS: current.TimeoutS,
|
||||
TargetID: current.TargetID,
|
||||
TargetType: current.TargetType,
|
||||
Zone: current.Zone,
|
||||
Enabled: current.Enabled,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Re-read to get updated timestamp.
|
||||
updated, err := sqlcgen.New(tx).GetCheckDef(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
check := checkDefToGen(updated)
|
||||
|
||||
actorType, actor := actorInfo(ctx)
|
||||
if auditErr := observability.Audit(ctx, sqlcgen.New(tx), actorType, actor, "patch",
|
||||
&id, "PATCH", "/api/v1/checks/"+req.Id, "",
|
||||
map[string]any{"enabled": updated.Enabled}); auditErr != nil {
|
||||
return nil, auditErr
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gen.PatchCheck200JSONResponse(check), nil
|
||||
}
|
||||
|
||||
func checkDefToGen(cd sqlcgen.CheckDef) gen.Check {
|
||||
c := gen.Check{
|
||||
Id: cd.EntityID,
|
||||
Kind: gen.CheckKind(cd.Kind),
|
||||
IntervalS: int(cd.IntervalS),
|
||||
TimeoutS: int(cd.TimeoutS),
|
||||
Enabled: cd.Enabled,
|
||||
TargetType: cd.TargetType,
|
||||
Zone: cd.Zone,
|
||||
}
|
||||
var config map[string]any
|
||||
if len(cd.Config) > 0 && json.Unmarshal(cd.Config, &config) == nil && len(config) > 0 {
|
||||
c.Config = &config
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// parseIntIfMatch parses an integer from a raw If-Match header value (with quotes stripped).
|
||||
func parseIntIfMatch(s string) (int, error) {
|
||||
if s == "" {
|
||||
return 0, fmt.Errorf("empty version")
|
||||
}
|
||||
var v int
|
||||
for _, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
return 0, fmt.Errorf("invalid version: %q", s)
|
||||
}
|
||||
v = v*10 + int(c-'0')
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
Reference in New Issue
Block a user