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.
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dtoro/oikos/internal/core/app"
|
||||
"github.com/dtoro/oikos/internal/core/ports"
|
||||
"github.com/dtoro/oikos/internal/adapters/postgres"
|
||||
"github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
func OpsTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
|
||||
func OpsTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets, execSvc *app.ExecutionService) []toolReg {
|
||||
return []toolReg{
|
||||
// ── request_execution (legacy fixed enum) retired 2026-07-14 ──
|
||||
// All mutations now route through `run`. The handler functions
|
||||
@@ -49,7 +50,7 @@ func OpsTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
|
||||
return textResult(fmt.Sprintf("target not found: %s", targetSlug)), nil
|
||||
}
|
||||
|
||||
return classifyAndGate(ctx, pool, agentID, targetID, targetSlug, command, purpose, declaredRisk, sessionID), nil
|
||||
return classifyAndGate(ctx, pool, execSvc, agentID, targetID, targetSlug, command, purpose, declaredRisk, sessionID), nil
|
||||
}},
|
||||
// docker_exec wraps a command inside a Docker container on an LXC.
|
||||
// Resolves the LXC, looks up the pve_id, and runs via
|
||||
@@ -101,7 +102,7 @@ func OpsTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
|
||||
noQuote := strings.ReplaceAll(command, "'", "'\\''")
|
||||
dockerCmd := fmt.Sprintf("docker exec %s sh -c '%s'", container, noQuote)
|
||||
|
||||
return classifyAndGate(ctx, pool, agentID, targetID, lxcSlug, dockerCmd, purpose, declaredRisk, sessionID), nil
|
||||
return classifyAndGate(ctx, pool, execSvc, agentID, targetID, lxcSlug, dockerCmd, purpose, declaredRisk, sessionID), nil
|
||||
}},
|
||||
// inspect_path is the bulk fact-gathering tool from
|
||||
// plans/2026-07-18-session-review-three-sessions.md P1.5.
|
||||
@@ -668,7 +669,7 @@ func OpsTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
|
||||
}
|
||||
command := fmt.Sprintf("systemctl restart %s", service)
|
||||
purpose := fmt.Sprintf("restart %s on %s", service, targetSlug)
|
||||
return classifyAndGate(ctx, pool, agentID, targetID, targetSlug, command, purpose, "config_mutation", sessionID), nil
|
||||
return classifyAndGate(ctx, pool, execSvc, agentID, targetID, targetSlug, command, purpose, "config_mutation", sessionID), nil
|
||||
}},
|
||||
{tool: &mcp.Tool{Name: "push_file", Description: "Copy a file from a Proxmox host into an LXC container via `pct push`. The source path must already exist on the Proxmox host that owns the LXC (stage it there first via `run` on the host, e.g. with scp/curl/wget). Classified config_mutation — requires operator approval. Prefer this over manual 3-hop SSH piping (`cat | ssh | pct exec tee`), which repeatedly drops into partial-write/text-file-busy states.",
|
||||
InputSchema: objSchema(
|
||||
@@ -725,7 +726,7 @@ func OpsTools(pool *db.Pool, agentID uuid.UUID, sec ports.Secrets) []toolReg {
|
||||
}
|
||||
}
|
||||
purpose := fmt.Sprintf("push %s into %s at %s", sourcePath, targetSlug, destPath)
|
||||
return classifyAndGate(ctx, pool, agentID, hostEntityID, "host:"+hostSlug, cmd, purpose, "config_mutation", sessionID), nil
|
||||
return classifyAndGate(ctx, pool, execSvc, agentID, hostEntityID, "host:"+hostSlug, cmd, purpose, "config_mutation", sessionID), nil
|
||||
}},
|
||||
// ── Approval Management (replaces Matrix notifier) ─────────────
|
||||
{tool: &mcp.Tool{Name: "list_approvals", Description: "List pending and recent approvals. Returns approval ID, action, risk class, target slug, status, and timing. Filter by status (pending, approved, denied) or entity slug to scope. Use after a `run` returns 'requires approval' to see what's pending so you can present it to the operator for a decision.",
|
||||
|
||||
Reference in New Issue
Block a user