Files
oikos/internal/mcp/tools.go
dtoro 7c9f4ec79f feat: Phase 4 governance/execution slice — PolicyService + ExecutionService
classifyAndGate's decision pipeline moves to core: PolicyService runs the
full gate order (classify + transport escalation, plan-first, syntax,
host-only/host-lxc, VM QGA preflight, dedup, approval-flood, window
routing) over ports.GovernanceStore; ExecutionService records and
dispatches (auto-run via ssh.CommandExecutor + TargetResolver, queue via
ExecutionRecorder) with one converged path for run/docker_exec. Gating
matrix test added (risk x window x declared risk -> outcome); pair
coverage 95.6%.

Bug fix surfaced by the matrix: the flag-space syntax regex was inverted
— it refused valid 'tail -n 3' and missed the actual 'head - n' typo.
Fixed to match dash-space-value only.

Remaining Phase 4 items tracked in the plan: ApprovalService.Decide
convergence, execlog fold, execworker poller. VERSION 0.35.0.
2026-08-16 09:48:26 +02:00

87 lines
2.2 KiB
Go

package mcp
import (
"context"
"fmt"
"strings"
"github.com/dtoro/oikos/internal/core/app"
"github.com/dtoro/oikos/internal/core/ports"
"github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/google/uuid"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type toolReg struct {
tool *mcp.Tool
handler toolHandler
}
func allTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets, entities *app.EntityService, relService *app.RelationshipService, execSvc *app.ExecutionService) []toolReg {
return append(append(append(append(
[]toolReg{},
EntityTools(pool, agentID, sec, entities, relService)...),
OpsTools(pool, agentID, sec, execSvc)...),
KnowledgeTools(pool, agentID, sec)...),
AnalysisTools(pool, agentID, sec)...)
}
func formatCheckResult(res app.DeriveResult) string {
var b strings.Builder
if res.Created > 0 {
fmt.Fprintf(&b, " Derived %d check(s).", res.Created)
}
if res.Undeclared {
b.WriteString(" Type declares no monitoring — no checks derived (set the entity's `monitoring` attribute and call update_entity_attributes to regenerate).")
}
for _, s := range res.Skipped {
fmt.Fprintf(&b, " Skipped %s (%s).", s.Kind, s.Reason)
}
return b.String()
}
func formatCreateResult(slug, entityType string, res app.DeriveResult) string {
return fmt.Sprintf("Created %s (%s).%s", slug, entityType, formatCheckResult(res))
}
func rowsToMap(ctx context.Context, pool *db.Pool, query string, args ...any) map[string]any {
m := map[string]any{}
rows, err := pool.Query(ctx, query, args...)
if err != nil {
return m
}
defer rows.Close()
for rows.Next() {
var key string
var val int
if rows.Scan(&key, &val) == nil {
m[key] = val
}
}
return m
}
func queryRowsJSONSingle(ctx context.Context, pool *db.Pool, query string, args ...any) []map[string]any {
rows, err := pool.Query(ctx, query, args...)
if err != nil {
return nil
}
defer rows.Close()
cols := rows.FieldDescriptions()
var items []map[string]any
for rows.Next() {
vals, err := rows.Values()
if err != nil {
continue
}
m := make(map[string]any)
for i, col := range cols {
m[string(col.Name)] = fmt.Sprintf("%v", vals[i])
}
items = append(items, m)
}
return items
}