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).
197 lines
5.7 KiB
Go
197 lines
5.7 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"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"
|
|
)
|
|
|
|
// ─── Skills ────────────────────────────────────────────────────────────
|
|
|
|
func (s *Server) ListSkills(ctx context.Context, req gen.ListSkillsRequestObject) (gen.ListSkillsResponseObject, error) {
|
|
limit := clampLimit(req.Params.Limit)
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT s.entity_id, s.version, s.name, s.procedure, s.applies_type,
|
|
s.action, s.pattern_ids, s.status, s.success_rate,
|
|
s.changed_by::text, s.change_reason, s.last_used_at
|
|
FROM skills s
|
|
WHERE ($1::text IS NULL OR s.status = $1)
|
|
AND ($2::text IS NULL OR s.applies_type = $2)
|
|
AND ($3::text IS NULL OR s.action = $3)
|
|
ORDER BY s.name, s.version DESC`,
|
|
req.Params.Status, req.Params.AppliesTo, req.Params.Action)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
// Deduplicate to latest version per skill (the ORDER BY name, version DESC
|
|
// means the first row per name is the latest).
|
|
seen := map[string]bool{}
|
|
items := []gen.Skill{}
|
|
for rows.Next() {
|
|
var s gen.Skill
|
|
var procBytes []byte
|
|
var patternIDs []uuid.UUID
|
|
if err := rows.Scan(&s.Id, &s.Version, &s.Name, &procBytes, &s.AppliesType,
|
|
&s.Action, &patternIDs, &s.Status, &s.SuccessRate,
|
|
&s.ChangedBy, &s.ChangeReason, &s.LastUsedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if seen[s.Id.String()] {
|
|
continue
|
|
}
|
|
seen[s.Id.String()] = true
|
|
if err := json.Unmarshal(procBytes, &s.Procedure); err != nil {
|
|
slog.Warn("phase3: unmarshal skill procedure", "skill", s.Name, "error", err)
|
|
}
|
|
if len(patternIDs) > 0 {
|
|
pids := make([]string, len(patternIDs))
|
|
for i, pid := range patternIDs {
|
|
pids[i] = pid.String()
|
|
}
|
|
s.PatternIds = &pids
|
|
}
|
|
items = append(items, s)
|
|
if len(items) > limit {
|
|
break
|
|
}
|
|
}
|
|
if rows.Err() != nil {
|
|
return nil, rows.Err()
|
|
}
|
|
|
|
if items == nil {
|
|
items = []gen.Skill{}
|
|
}
|
|
return gen.ListSkills200JSONResponse{Items: items}, nil
|
|
}
|
|
|
|
func (s *Server) PatchSkill(ctx context.Context, req gen.PatchSkillRequestObject) (gen.PatchSkillResponseObject, 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 {
|
|
if err := q.UpdateSkillStatus(ctx, sqlcgen.UpdateSkillStatusParams{
|
|
EntityID: id,
|
|
Status: string(*req.Body.Status),
|
|
}); err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return nil, fmt.Errorf("%w: skill %s", domain.ErrNotFound, req.Id)
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Re-read skill.
|
|
var skill gen.Skill
|
|
var procBytes []byte
|
|
var patternIDs []uuid.UUID
|
|
err = tx.QueryRow(ctx, `
|
|
SELECT entity_id, version, name, procedure, applies_type, action,
|
|
pattern_ids, status, success_rate, changed_by::text,
|
|
change_reason, last_used_at
|
|
FROM skills WHERE entity_id = $1 ORDER BY version DESC LIMIT 1`, id).
|
|
Scan(&skill.Id, &skill.Version, &skill.Name, &procBytes, &skill.AppliesType,
|
|
&skill.Action, &patternIDs, &skill.Status, &skill.SuccessRate,
|
|
&skill.ChangedBy, &skill.ChangeReason, &skill.LastUsedAt)
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return nil, fmt.Errorf("%w: skill %s", domain.ErrNotFound, req.Id)
|
|
}
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(procBytes, &skill.Procedure); err != nil {
|
|
slog.Warn("phase3: unmarshal skill proc", "error", err)
|
|
}
|
|
if len(patternIDs) > 0 {
|
|
pids := make([]string, len(patternIDs))
|
|
for i, pid := range patternIDs {
|
|
pids[i] = pid.String()
|
|
}
|
|
skill.PatternIds = &pids
|
|
}
|
|
|
|
actorType, actor := actorInfo(ctx)
|
|
if auditErr := observability.Audit(ctx, q, actorType, actor, "patch",
|
|
&id, "PATCH", "/api/v1/skills/"+req.Id, "",
|
|
map[string]any{"status": req.Body.Status, "pinned_version": req.Body.PinnedVersion}); auditErr != nil {
|
|
return nil, auditErr
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return gen.PatchSkill200JSONResponse(skill), nil
|
|
}
|
|
|
|
func (s *Server) ListSkillVersions(ctx context.Context, req gen.ListSkillVersionsRequestObject) (gen.ListSkillVersionsResponseObject, error) {
|
|
id, err := s.resolveEntityID(ctx, req.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT entity_id, version, name, procedure, applies_type, action,
|
|
pattern_ids, status, success_rate, changed_by::text,
|
|
change_reason, last_used_at
|
|
FROM skills WHERE entity_id = $1
|
|
ORDER BY version DESC`, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := []gen.Skill{}
|
|
for rows.Next() {
|
|
var skill gen.Skill
|
|
var procBytes []byte
|
|
var patternIDs []uuid.UUID
|
|
if err := rows.Scan(&skill.Id, &skill.Version, &skill.Name, &procBytes, &skill.AppliesType,
|
|
&skill.Action, &patternIDs, &skill.Status, &skill.SuccessRate,
|
|
&skill.ChangedBy, &skill.ChangeReason, &skill.LastUsedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(procBytes, &skill.Procedure); err != nil {
|
|
slog.Warn("phase3: unmarshal skill proc", "error", err)
|
|
}
|
|
if len(patternIDs) > 0 {
|
|
pids := make([]string, len(patternIDs))
|
|
for i, pid := range patternIDs {
|
|
pids[i] = pid.String()
|
|
}
|
|
skill.PatternIds = &pids
|
|
}
|
|
items = append(items, skill)
|
|
}
|
|
if rows.Err() != nil {
|
|
return nil, rows.Err()
|
|
}
|
|
if items == nil {
|
|
items = []gen.Skill{}
|
|
}
|
|
return gen.ListSkillVersions200JSONResponse{Items: items}, nil
|
|
}
|