feat: assent window + compound read-only classification + continue-after-approval
Agent stopped after every approval step, forcing operator to type 'continue' 7× per deploy session. Root causes and fixes: 1. Compound read-only commands (e.g. 'systemctl status; journalctl') defaulted to config_mutation — now splits on ;/&&/||/| and classifies as read_only if all segments are inspection verbs. Added grep, wc, sort, uniq, cut, tr, dpkg -l, apt list, docker stats to allowlist. 2. curl|sh was classified destructive, forcing typed confirmation for legitimate installs (get.docker.com). Demoted to config_mutation — loose assent grants it, no typed phrase needed. 3. SOUL.md said 'STOP after queuing' — replaced with 'continue working on non-blocked steps'. Added assent window section instructing agent to carry out the full plan after approval. 4. Assent window: when operator approves a plan via chat assent, a 30-minute window opens where config_mutation commands auto-run without re-approval. Agent writes expiry to autonomy_settings; MCP run tool checks it before gating. Destructive never auto-runs. 5. System note after approval now says 'CONTINUE executing the full plan — do not stop and wait for continue.'
This commit is contained in:
@@ -114,6 +114,32 @@ You have access to MCP tools to query topology, health, knowledge, and request
|
||||
gated mutations through request_execution. Be concise. Prefer tools over guessing.`
|
||||
}
|
||||
|
||||
// assentWindowDuration is how long after an operator approves a plan that
|
||||
// config_mutation commands auto-run without re-approval. The operator
|
||||
// approved the plan; the agent should execute it end-to-end without
|
||||
// stopping every step to re-ask. Destructive actions still always need
|
||||
// explicit typed confirmation regardless of the window.
|
||||
const assentWindowDuration = 30 * time.Minute
|
||||
|
||||
// openAssentWindow records an active assent window in autonomy_settings so
|
||||
// the MCP run tool (separate process) can check it before requiring approval
|
||||
// for config_mutation commands. Key is scoped to this agent's UUID.
|
||||
func (a *agent) openAssentWindow(ctx context.Context) {
|
||||
if a.store == nil || a.store.pool == nil || a.agentID == uuid.Nil {
|
||||
return
|
||||
}
|
||||
key := "assent_window.agent:" + a.agentID.String()
|
||||
expires := time.Now().Add(assentWindowDuration).UTC().Format(time.RFC3339)
|
||||
_, err := a.store.pool.Exec(ctx,
|
||||
`INSERT INTO autonomy_settings (key, value) VALUES ($1, $2)
|
||||
ON CONFLICT (key) DO UPDATE SET value = $2`, key, expires)
|
||||
if err != nil {
|
||||
slog.Warn("nomos: openAssentWindow", "error", err)
|
||||
} else {
|
||||
slog.Info("nomos: assent window opened", "agent", a.agentID, "expires", expires)
|
||||
}
|
||||
}
|
||||
|
||||
type toolDef struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
@@ -199,7 +225,8 @@ func (a *agent) chat(ctx context.Context, sessionID, message string, emit func(a
|
||||
}
|
||||
}
|
||||
if len(granted) > 0 {
|
||||
note := fmt.Sprintf("[System: the operator's message approved pending execution(s) %s via chat assent — they are now running. Do not call request_execution/run again for these; check get_execution_status if you need the outcome before replying.]", strings.Join(granted, ", "))
|
||||
a.openAssentWindow(ctx)
|
||||
note := fmt.Sprintf("[System: the operator's message approved pending execution(s) %s via chat assent — they are now running. An assent window is now active for 30 minutes: config_mutation commands will auto-run without re-approval. Do not re-request or call request_execution/run again for these; check get_execution_status if you need the outcome. CONTINUE executing the full plan — do not stop and wait for 'continue' after each step. Only surface to the operator for destructive actions (need typed confirmation) or if you're genuinely stuck after trying alternatives.]", strings.Join(granted, ", "))
|
||||
messages = append(messages, openai.SystemMessage(note))
|
||||
}
|
||||
if len(blocked) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user