diff --git a/VERSION b/VERSION index 0f82685..6678432 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.7 +0.3.8 diff --git a/cmd/nomos/store.go b/cmd/nomos/store.go index aab9570..d2c2f33 100644 --- a/cmd/nomos/store.go +++ b/cmd/nomos/store.go @@ -464,6 +464,15 @@ func (s *store) proposePlan(ctx context.Context, sessionID string, steps []planS // rather than replace it (mirrors the mid-flight append above). _ = observability.Event(ctx, sqlcgen.New(s.pool), "plan.proposed", s.taskEntityPtr(ctx, sessionID), "info", "nomos", sessionID, map[string]any{"steps": out, "appended": anyStarted}) + // Record a plan-proposed window so the `run` handler knows a plan is + // pending approval and can skip per-action approval for config_mutation + // commands within the plan. Transitions to 'active' when the operator + // approves (chat-assent or button). The window key is session-scoped; + // one agent serves all sessions on this nomos instance. + s.pool.Exec(ctx, + `INSERT INTO autonomy_settings (key, value) VALUES ($1, 'proposed') + ON CONFLICT (key) DO UPDATE SET value = 'proposed'`, + "nomos:plan:"+sessionID) return out, nil } diff --git a/internal/mcp/server.go b/internal/mcp/server.go index f643370..ecfe9e4 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -1313,6 +1313,26 @@ func classifyAndGate(ctx context.Context, pool *db.Pool, agentID, targetID uuid. return textResult(fmt.Sprintf("run on %s (read_only, auto): %s", targetSlug, out)) } + // Plan window: if a plan was proposed (and possibly approved), this + // command is part of an in-flight plan. The plan IS the approval — + // config_mutation commands within an active plan auto-run without + // per-action approval. Created by propose_plan, checked by planWindowActive. + if riskClass == policy.RiskConfigMutation && sessionID != "" && planWindowActive(ctx, pool, sessionID) { + host, user, wrap, rerr := resolveExecTarget(ctx, pool, targetSlug) + if rerr != nil { + pool.Exec(ctx, `UPDATE executions SET status='failed', result=$2::jsonb WHERE entity_id=$1`, id, jsonErr("%s", rerr.Error())) + return textResult(fmt.Sprintf("resolve target: %v", rerr)) + } + out, xerr := sshExec(ctx, host, user, wrap(command)) + if xerr != nil { + pool.Exec(ctx, `UPDATE executions SET status='failed', result=$2::jsonb WHERE entity_id=$1`, id, jsonErr("%s: %s", xerr.Error(), out)) + return textResult(fmt.Sprintf("run on %s: ERROR %v\n%s", targetSlug, xerr, out)) + } + pool.Exec(ctx, `UPDATE executions SET status='completed', result=$2::jsonb WHERE entity_id=$1`, id, jsonOut(out)) + slog.Info("mcp: run auto-executed via plan window", "target", targetSlug, "execution_id", id) + return textResult(fmt.Sprintf("run on %s (config_mutation, auto via plan): %s", targetSlug, out)) + } + // Assent window: if the operator recently approved a plan in this // agent's chat session, config_mutation commands auto-run without // re-approval. This is the "approve the plan, carry it out" path — the @@ -1408,6 +1428,21 @@ func executeApprovedViaAPI(ctx context.Context, execID uuid.UUID, targetSlug, ac } } +// planWindowActive reports whether a plan has been proposed (or approved) +// for this session. Created by propose_plan, the window allows config_mutation +// commands to auto-execute without per-action approval — the plan IS the +// approval. Plan-approve-once policy (2026-07-14). +func planWindowActive(ctx context.Context, pool *db.Pool, sessionID string) bool { + if sessionID == "" { + return false + } + var val string + err := pool.QueryRow(ctx, + "SELECT value FROM autonomy_settings WHERE key = $1", + "nomos:plan:"+sessionID).Scan(&val) + return err == nil && (val == "proposed" || val == "active") +} + // assentWindowActive checks whether the operator has recently approved a plan // in THIS TASK's chat session. The agent sets an // assent_window.agent:.session: key in autonomy_settings with an diff --git a/web/src/lib/components/ActivityTimeline.svelte b/web/src/lib/components/ActivityTimeline.svelte index bdb2f2c..692d1e3 100644 --- a/web/src/lib/components/ActivityTimeline.svelte +++ b/web/src/lib/components/ActivityTimeline.svelte @@ -43,8 +43,13 @@
{#if $activityLog.length === 0} -
- Waiting for agent activity… +
+
+
+
+
+
+

Waiting for activity…

{:else}
diff --git a/web/src/lib/components/SessionGraph.svelte b/web/src/lib/components/SessionGraph.svelte index 8c15a3f..1e4c48f 100644 --- a/web/src/lib/components/SessionGraph.svelte +++ b/web/src/lib/components/SessionGraph.svelte @@ -300,12 +300,6 @@