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).
124 lines
3.6 KiB
Go
124 lines
3.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"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/jackc/pgx/v5"
|
|
)
|
|
|
|
// ─── Patterns ──────────────────────────────────────────────────────────
|
|
|
|
func (s *Server) ListPatterns(ctx context.Context, req gen.ListPatternsRequestObject) (gen.ListPatternsResponseObject, error) {
|
|
limit := clampLimit(req.Params.Limit)
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT p.entity_id, e.slug, p.applies_type, p.action, p.pattern, p.confidence,
|
|
p.evidence_count, p.success_count, p.failure_count, p.status,
|
|
p.quarantined, p.version, p.last_validated_at
|
|
FROM patterns p
|
|
JOIN entities e ON e.id = p.entity_id
|
|
WHERE ($1::text IS NULL OR p.status = $1)
|
|
AND ($2::text IS NULL OR p.applies_type = $2)
|
|
AND ($3::text IS NULL OR p.action = $3)
|
|
ORDER BY p.applies_type, p.action
|
|
LIMIT $4`,
|
|
req.Params.Status, req.Params.EntityType, req.Params.Action, limit+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := []gen.Pattern{}
|
|
for rows.Next() {
|
|
var p gen.Pattern
|
|
if err := rows.Scan(&p.Id, &p.Slug, &p.AppliesType, &p.Action, &p.Pattern,
|
|
&p.Confidence, &p.EvidenceCount, &p.SuccessCount, &p.FailureCount,
|
|
&p.Status, &p.Quarantined, &p.Version, &p.LastValidatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, p)
|
|
}
|
|
if rows.Err() != nil {
|
|
return nil, rows.Err()
|
|
}
|
|
|
|
if items == nil {
|
|
items = []gen.Pattern{}
|
|
}
|
|
return gen.ListPatterns200JSONResponse{Items: items}, nil
|
|
}
|
|
|
|
func (s *Server) PatchPattern(ctx context.Context, req gen.PatchPatternRequestObject) (gen.PatchPatternResponseObject, 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
|
|
}
|
|
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
q := sqlcgen.New(tx)
|
|
|
|
if req.Body.Status != nil {
|
|
status := string(*req.Body.Status)
|
|
if err := q.UpdatePatternStatus(ctx, sqlcgen.UpdatePatternStatusParams{
|
|
EntityID: id,
|
|
Status: status,
|
|
}); err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return nil, fmt.Errorf("%w: pattern %s", domain.ErrNotFound, req.Id)
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if req.Body.Quarantined != nil {
|
|
if err := q.UpdatePatternQuarantine(ctx, sqlcgen.UpdatePatternQuarantineParams{
|
|
EntityID: id,
|
|
Quarantined: *req.Body.Quarantined,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Re-read.
|
|
var p gen.Pattern
|
|
err = tx.QueryRow(ctx, `
|
|
SELECT entity_id, applies_type, action, pattern, confidence,
|
|
evidence_count, success_count, failure_count, status,
|
|
quarantined, version, last_validated_at
|
|
FROM patterns WHERE entity_id = $1`, id).
|
|
Scan(&p.Id, &p.AppliesType, &p.Action, &p.Pattern,
|
|
&p.Confidence, &p.EvidenceCount, &p.SuccessCount, &p.FailureCount,
|
|
&p.Status, &p.Quarantined, &p.Version, &p.LastValidatedAt)
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return nil, fmt.Errorf("%w: pattern %s", domain.ErrNotFound, req.Id)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
actorType, actor := actorInfo(ctx)
|
|
if auditErr := observability.Audit(ctx, q, actorType, actor, "patch",
|
|
&id, "PATCH", "/api/v1/patterns/"+req.Id, "",
|
|
map[string]any{"status": req.Body.Status, "quarantined": req.Body.Quarantined}); auditErr != nil {
|
|
return nil, auditErr
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return gen.PatchPattern200JSONResponse(p), nil
|
|
}
|