feat: dsh-gate — auto-run non-destructive mutations when no session ID

PolicyService.route now auto-runs config_mutation and reversible_low when
sessionID is empty (dsh sidecar path), routing as 'dsh-gate'. Destructive
still queues. This eliminates the two-layer consent issue: dsh pre-execute
is the sole consent gate, oikos no longer queues dsh-originated mutations.

Also updates the dsh plan with Phase 1-3 completion status and the
architectural decision that _session_id injection is permanently deferred.

v0.38.0
This commit is contained in:
2026-08-16 17:03:27 +02:00
parent 04aa1bd5e8
commit eca81ae9af
4 changed files with 69 additions and 62 deletions

View File

@@ -144,6 +144,26 @@ func TestPlanFirstGate(t *testing.T) {
}
}
func TestNoSessionAutoRunExceptDestructive(t *testing.T) {
svc, _ := newPolicySvc(t)
noSession := func(cmd, risk string) PolicySubmitInput {
return PolicySubmitInput{AgentID: agentID, TargetID: targetID, TargetSlug: "lxc:x", Command: cmd, DeclaredRisk: risk}
}
// config_mutation with no session → auto-run (dsh owns consent)
d := svc.Decide(context.Background(), noSession("systemctl restart caddy", ""))
if d.Action != DecisionAuto {
t.Errorf("no-session config_mutation = %+v, want auto", d)
}
if d.AutoViaWindow != "dsh-gate" {
t.Errorf("no-session config_mutation via = %q, want dsh-gate", d.AutoViaWindow)
}
// destructive with no session → still queue
d = svc.Decide(context.Background(), noSession("rm -rf /data", ""))
if d.Action != DecisionQueue {
t.Errorf("no-session destructive = %+v, want queue", d)
}
}
func TestSyntaxValidationGate(t *testing.T) {
svc, _ := newPolicySvc(t)
for _, bad := range []string{