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:
160
internal/httpapi/approval_rules.go
Normal file
160
internal/httpapi/approval_rules.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
)
|
||||
|
||||
// ─── Approval Rules (Policy) ───────────────────────────────────────────
|
||||
|
||||
func (s *Server) ListApprovalRules(ctx context.Context, req gen.ListApprovalRulesRequestObject) (gen.ListApprovalRulesResponseObject, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, entity_type, action, risk_class, autonomy_level,
|
||||
COALESCE((SELECT slug FROM entities WHERE id = scope_entity), ''),
|
||||
version, updated_at
|
||||
FROM approval_rules ORDER BY entity_type, action`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []gen.ApprovalRule{}
|
||||
for rows.Next() {
|
||||
var rule gen.ApprovalRule
|
||||
var scopeSlug string
|
||||
if err := rows.Scan(&rule.Id, &rule.EntityType, &rule.Action,
|
||||
&rule.RiskClass, &rule.AutonomyLevel, &scopeSlug,
|
||||
&rule.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if scopeSlug != "" {
|
||||
rule.ScopeEntity = &scopeSlug
|
||||
}
|
||||
items = append(items, rule)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, rows.Err()
|
||||
}
|
||||
if items == nil {
|
||||
items = []gen.ApprovalRule{}
|
||||
}
|
||||
return gen.ListApprovalRules200JSONResponse{Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateApprovalRule(ctx context.Context, req gen.CreateApprovalRuleRequestObject) (gen.CreateApprovalRuleResponseObject, 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
|
||||
}
|
||||
|
||||
var scopeEntity *uuid.UUID
|
||||
if req.Body.ScopeEntity != nil && *req.Body.ScopeEntity != "" {
|
||||
se, rerr := s.resolveEntityID(ctx, *req.Body.ScopeEntity)
|
||||
if rerr != nil {
|
||||
return nil, rerr
|
||||
}
|
||||
scopeEntity = &se
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
_, err = tx.Exec(ctx, `
|
||||
INSERT INTO approval_rules (id, entity_type, action, risk_class, autonomy_level, scope_entity)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
id, req.Body.EntityType, req.Body.Action, req.Body.RiskClass,
|
||||
string(req.Body.AutonomyLevel), scopeEntity)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "duplicate") {
|
||||
return nil, fmt.Errorf("%w: rule for %s/%s already exists", domain.ErrAlreadyExists,
|
||||
coalesceStr(req.Body.EntityType, "*"), req.Body.Action)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorType, actor := actorInfo(ctx)
|
||||
if auditErr := observability.Audit(ctx, sqlcgen.New(tx), actorType, actor, "create",
|
||||
&id, "POST", "/api/v1/policy/approval-rules", "",
|
||||
map[string]any{"action": req.Body.Action, "risk_class": req.Body.RiskClass}); auditErr != nil {
|
||||
return nil, auditErr
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return 202 pending approval (dual-control).
|
||||
return gen.CreateApprovalRule202JSONResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) PatchApprovalRule(ctx context.Context, req gen.PatchApprovalRuleRequestObject) (gen.PatchApprovalRuleResponseObject, 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
|
||||
}
|
||||
|
||||
var scopeEntity *uuid.UUID
|
||||
if req.Body.ScopeEntity != nil && *req.Body.ScopeEntity != "" {
|
||||
se, rerr := s.resolveEntityID(ctx, *req.Body.ScopeEntity)
|
||||
if rerr != nil {
|
||||
return nil, rerr
|
||||
}
|
||||
scopeEntity = &se
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
result, err := tx.Exec(ctx, `
|
||||
UPDATE approval_rules
|
||||
SET entity_type = COALESCE($2, entity_type),
|
||||
action = COALESCE($3, action),
|
||||
risk_class = COALESCE($4, risk_class),
|
||||
autonomy_level = COALESCE($5, autonomy_level),
|
||||
scope_entity = COALESCE($6, scope_entity),
|
||||
version = version + 1,
|
||||
updated_at = now()
|
||||
WHERE id = $1`,
|
||||
id, req.Body.EntityType, req.Body.Action, req.Body.RiskClass,
|
||||
string(req.Body.AutonomyLevel), scopeEntity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return nil, fmt.Errorf("%w: approval rule %s", domain.ErrNotFound, req.Id)
|
||||
}
|
||||
|
||||
actorType, actor := actorInfo(ctx)
|
||||
if auditErr := observability.Audit(ctx, sqlcgen.New(tx), actorType, actor, "patch",
|
||||
&id, "PATCH", "/api/v1/policy/approval-rules/"+req.Id, "",
|
||||
map[string]any{"action": req.Body.Action}); auditErr != nil {
|
||||
return nil, auditErr
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gen.PatchApprovalRule202JSONResponse{}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user