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.
175 lines
6.4 KiB
Go
175 lines
6.4 KiB
Go
package ports
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/dtoro/oikos/internal/core/domain"
|
|
)
|
|
|
|
// SignalUpsertInput is the observe-pass signal write: open/resolve
|
|
// transitions with their triggers, committed atomically.
|
|
type SignalUpsertInput struct {
|
|
Signal domain.Signal
|
|
Triggers []SignalTrigger
|
|
Audit []AuditEntry
|
|
Event *Event
|
|
}
|
|
|
|
// SignalTrigger is an automatic follow-up fired on a signal transition
|
|
// (payload firm; shapes firm up with the Phase 5 observation slice).
|
|
type SignalTrigger struct {
|
|
Kind string
|
|
EntityID domain.UUID
|
|
Parameters map[string]any
|
|
}
|
|
|
|
// SignalTransitionInput acks/resolves/mutes a signal (check-then-act on the
|
|
// signal's current state inside the transaction).
|
|
type SignalTransitionInput struct {
|
|
SignalID domain.UUID
|
|
Action string // "ack", "resolve", "mute"
|
|
Note string
|
|
MuteFor time.Duration
|
|
Actor string
|
|
Audit []AuditEntry
|
|
Event *Event
|
|
}
|
|
|
|
// SignalRepository is the signal aggregate.
|
|
type SignalRepository interface {
|
|
Open(ctx context.Context) ([]domain.Signal, error)
|
|
History(ctx context.Context, entityID domain.UUID, limit int) ([]domain.Signal, error)
|
|
|
|
UpsertWithTriggers(ctx context.Context, input SignalUpsertInput) error
|
|
Transition(ctx context.Context, input SignalTransitionInput) (domain.Signal, error)
|
|
}
|
|
|
|
// ExecutionSubmitInput is the queued-execution write: execution row,
|
|
// approval (for gated risk classes), audit, and event — one transaction.
|
|
type ExecutionSubmitInput struct {
|
|
Execution domain.Execution
|
|
Approval *domain.Approval
|
|
Audit []AuditEntry
|
|
Event *Event
|
|
}
|
|
|
|
// ExecutionCompleteInput closes out an execution: final status, output
|
|
// summary, audit, event.
|
|
type ExecutionCompleteInput struct {
|
|
ExecutionID domain.UUID
|
|
Status string
|
|
Output string
|
|
ExitCode int
|
|
Audit []AuditEntry
|
|
Event *Event
|
|
}
|
|
|
|
// ExecutionRepository is the execution aggregate. Claim uses an advisory
|
|
// lock so exactly one worker claims a queued execution.
|
|
type ExecutionRepository interface {
|
|
List(ctx context.Context, cursor string, limit int) ([]domain.Execution, error)
|
|
ReadLog(ctx context.Context, executionID domain.UUID) ([]string, error)
|
|
|
|
SubmitQueued(ctx context.Context, input ExecutionSubmitInput) (domain.Execution, error)
|
|
Claim(ctx context.Context) (*domain.Execution, error)
|
|
AppendLog(ctx context.Context, executionID domain.UUID, chunk string) error
|
|
Complete(ctx context.Context, input ExecutionCompleteInput) error
|
|
}
|
|
|
|
// ApprovalDecideInput verifies the HMAC token (check-then-act), flips the
|
|
// approval, un-gates the execution, and appends audit — one transaction.
|
|
// Double-approve must not double-execute.
|
|
type ApprovalDecideInput struct {
|
|
ApprovalID domain.UUID
|
|
Token string
|
|
Approved bool
|
|
Actor string
|
|
Audit []AuditEntry
|
|
Event *Event
|
|
}
|
|
|
|
// ApprovalRepository is the approval aggregate.
|
|
type ApprovalRepository interface {
|
|
ListPending(ctx context.Context, entityID domain.UUID, limit int) ([]domain.Approval, error)
|
|
|
|
Decide(ctx context.Context, input ApprovalDecideInput) (domain.Approval, error)
|
|
}
|
|
|
|
// GovernanceStore is the policy read model: the facts the gating decision
|
|
// needs that live outside the command text — session plan state, assent/
|
|
// destructive windows, duplicate pending executions, and target entity
|
|
// attributes. Implemented by the postgres adapter; faked in portstest.
|
|
type GovernanceStore interface {
|
|
// SessionHasPlan reports whether the session has any non-replaced plan
|
|
// step (the plan-first gate). Fails open on store errors — a flake
|
|
// must not block otherwise-valid work.
|
|
SessionHasPlan(ctx context.Context, sessionID string) bool
|
|
// AssentWindowActive reports a live operator-assent grant for this
|
|
// agent+session (config_mutation auto-run scope).
|
|
AssentWindowActive(ctx context.Context, agentID domain.UUID, sessionID string) bool
|
|
// DestructiveWindowActive reports a live explicitly-confirmed
|
|
// destructive grant for this agent+target+session.
|
|
DestructiveWindowActive(ctx context.Context, agentID domain.UUID, targetSlug, sessionID string) bool
|
|
// PendingApprovalCount counts this session's executions stuck at
|
|
// pending_approval (the approval-flood gate).
|
|
PendingApprovalCount(ctx context.Context, sessionID string) int
|
|
// PendingDuplicate returns the execution id of an identical command
|
|
// already queued for approval on this target, if any.
|
|
PendingDuplicate(ctx context.Context, targetID domain.UUID, action string) (domain.UUID, bool)
|
|
// EntityAttributes loads one entity's attributes map (VM QEMU-guest-
|
|
// agent state and similar gating inputs).
|
|
EntityAttributes(ctx context.Context, targetID domain.UUID) (map[string]any, error)
|
|
}
|
|
|
|
// CreateRunInput records one `run` submission in the execution ledger:
|
|
// the execution entity + row, its graph edges, the classification
|
|
// decision, and the audit entry — one transaction. Written before the
|
|
// route executes (auto-run or queue), exactly as the pre-service
|
|
// classifyAndGate path did.
|
|
type CreateRunInput struct {
|
|
ExecutionID domain.UUID
|
|
TargetID domain.UUID
|
|
TargetSlug string
|
|
AgentID domain.UUID
|
|
SessionID string
|
|
Command string
|
|
Purpose string
|
|
Action string // "run:{json params}" action column value
|
|
RiskClass string
|
|
Route string // "auto-act" | "escalate"
|
|
ClassReasoning []byte
|
|
CorrelationID string
|
|
}
|
|
|
|
// QueueApprovalInput records the operator-approval ask for a gated
|
|
// execution: the approval row, the execution status flip, and the
|
|
// session's awaiting_input transition (with its task.status event).
|
|
type QueueApprovalInput struct {
|
|
ExecutionID domain.UUID
|
|
TargetID domain.UUID
|
|
TargetSlug string
|
|
Action string
|
|
Params string
|
|
RiskClass string
|
|
SessionID string
|
|
}
|
|
|
|
// FinalizeExecutionInput closes an auto-run execution with full timing.
|
|
type FinalizeExecutionInput struct {
|
|
ExecutionID domain.UUID
|
|
Status string // "completed" | "failed"
|
|
Result []byte
|
|
StartedAt time.Time
|
|
}
|
|
|
|
// ExecutionRecorder persists the run-submission lifecycle. The service
|
|
// owns ordering and the decision; the adapter owns the SQL (entity row,
|
|
// executions row, edges, classifications, approvals, audit, events).
|
|
type ExecutionRecorder interface {
|
|
CreateRun(ctx context.Context, input CreateRunInput) error
|
|
MarkRunning(ctx context.Context, executionID domain.UUID) error
|
|
Finalize(ctx context.Context, input FinalizeExecutionInput) error
|
|
QueueApproval(ctx context.Context, input QueueApprovalInput) error
|
|
}
|