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).
86 lines
2.6 KiB
Go
86 lines
2.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/dtoro/oikos/internal/httpapi/gen"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ─── Classifications ───────────────────────────────────────────────────
|
|
|
|
func (s *Server) ListClassifications(ctx context.Context, req gen.ListClassificationsRequestObject) (gen.ListClassificationsResponseObject, error) {
|
|
limit := clampLimit(req.Params.Limit)
|
|
var route *string
|
|
if req.Params.Route != nil {
|
|
r := string(*req.Params.Route)
|
|
route = &r
|
|
}
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT c.entity_id, c.signal_entity_id, c.target_entity_id, c.action,
|
|
c.recommended_action, c.risk_class, c.route, c.blast_radius,
|
|
c.pattern_confidence, c.skill_id, c.autonomy_check, c.reasoning,
|
|
c.correlation_id, c.created_at,
|
|
e.slug, COALESCE(se.slug, '') AS signal_slug, COALESCE(te.slug, '') AS target_slug
|
|
FROM classifications c
|
|
LEFT JOIN entities e ON e.id = c.entity_id
|
|
LEFT JOIN entities se ON se.id = c.signal_entity_id
|
|
LEFT JOIN entities te ON te.id = c.target_entity_id
|
|
WHERE ($1::text IS NULL OR c.route = $1)
|
|
AND ($2::text IS NULL OR e.slug > $2)
|
|
ORDER BY e.slug
|
|
LIMIT $3`,
|
|
route, req.Params.Cursor, limit+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := []gen.Classification{}
|
|
for rows.Next() {
|
|
var cls gen.Classification
|
|
var recActionJSON []byte
|
|
var reasoningJSON []byte
|
|
var blastRadius []uuid.UUID
|
|
var signalSlug, targetSlug string
|
|
if err := rows.Scan(&cls.Id, &cls.SignalId, &targetSlug, &cls.Action,
|
|
&recActionJSON, &cls.RiskClass, &cls.Route, &blastRadius,
|
|
&cls.PatternConfidence, &cls.SkillId, &cls.AutonomyCheck, &reasoningJSON,
|
|
&cls.CorrelationId, &cls.CreatedAt,
|
|
&cls.Target, &signalSlug, &targetSlug); err != nil {
|
|
return nil, err
|
|
}
|
|
if targetSlug != "" {
|
|
cls.Target = &targetSlug
|
|
}
|
|
var reasoning map[string]any
|
|
if json.Unmarshal(reasoningJSON, &reasoning) == nil {
|
|
cls.Reasoning = reasoning
|
|
}
|
|
if len(blastRadius) > 0 {
|
|
br := make([]string, len(blastRadius))
|
|
for i, id := range blastRadius {
|
|
br[i] = id.String()
|
|
}
|
|
cls.BlastRadius = &br
|
|
}
|
|
items = append(items, cls)
|
|
}
|
|
if rows.Err() != nil {
|
|
return nil, rows.Err()
|
|
}
|
|
|
|
var next *string
|
|
if len(items) > limit {
|
|
items = items[:limit]
|
|
if items[len(items)-1].Target != nil {
|
|
next = items[len(items)-1].Target
|
|
}
|
|
}
|
|
if items == nil {
|
|
items = []gen.Classification{}
|
|
}
|
|
return gen.ListClassifications200JSONResponse{Items: items, NextCursor: next}, nil
|
|
}
|