diff --git a/cmd/pulse-agent/main.go b/cmd/pulse-agent/main.go index a403f6eb1..2e694476f 100644 --- a/cmd/pulse-agent/main.go +++ b/cmd/pulse-agent/main.go @@ -426,7 +426,8 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { ModuleStatus: runtimeStatus.moduleStatuses, Observers: hostObserverTargets(cfg.Observers), - DockerContainerUpdater: dockerUpdaterBridge, + DockerContainerUpdater: dockerUpdaterBridge, + DockerContainerLifecycleOperator: dockerUpdaterBridge, } wireUpdaterHooks(&hostCfg, updater) @@ -1473,8 +1474,9 @@ func initModuleWithRetry[T any](ctx context.Context, logger *zerolog.Logger, com // Docker module comes up (or never does). The host command client holds this // bridge for the process lifetime; set installs the module's implementation. type lateBoundDockerUpdater struct { - mu sync.RWMutex - updater hostagent.DockerContainerUpdater + mu sync.RWMutex + updater hostagent.DockerContainerUpdater + lifecycle hostagent.DockerContainerLifecycleOperator } func (b *lateBoundDockerUpdater) set(candidate any) { @@ -1483,11 +1485,37 @@ func (b *lateBoundDockerUpdater) set(candidate any) { fmt.Fprintf(os.Stderr, "docker update bridge: %T does not implement the typed container updater\n", candidate) return } + lifecycle, ok := candidate.(hostagent.DockerContainerLifecycleOperator) + if !ok { + fmt.Fprintf(os.Stderr, "docker lifecycle bridge: %T does not implement the typed container lifecycle operator\n", candidate) + return + } b.mu.Lock() b.updater = updater + b.lifecycle = lifecycle b.mu.Unlock() } +func (b *lateBoundDockerUpdater) InspectDockerContainerLifecycle(ctx context.Context, runtime, containerID string) (agentexec.DockerContainerLifecycleSnapshot, error) { + b.mu.RLock() + lifecycle := b.lifecycle + b.mu.RUnlock() + if lifecycle == nil { + return agentexec.DockerContainerLifecycleSnapshot{}, fmt.Errorf("docker module is not running on this agent") + } + return lifecycle.InspectDockerContainerLifecycle(ctx, runtime, containerID) +} + +func (b *lateBoundDockerUpdater) MutateDockerContainerLifecycle(ctx context.Context, runtime, operation, containerID string) error { + b.mu.RLock() + lifecycle := b.lifecycle + b.mu.RUnlock() + if lifecycle == nil { + return fmt.Errorf("docker module is not running on this agent") + } + return lifecycle.MutateDockerContainerLifecycle(ctx, runtime, operation, containerID) +} + func (b *lateBoundDockerUpdater) TypedContainerUpdate(ctx context.Context, runtime, containerID, expectedImageDigest string, progress func(string)) (agentexec.DockerContainerUpdateOutcome, error) { b.mu.RLock() updater := b.updater diff --git a/cmd/pulse-agent/main_test.go b/cmd/pulse-agent/main_test.go index 663f6c375..daa0f628a 100644 --- a/cmd/pulse-agent/main_test.go +++ b/cmd/pulse-agent/main_test.go @@ -2300,7 +2300,9 @@ func TestSecureAgentStateDir(t *testing.T) { } type stubTypedContainerUpdater struct { - calls int + calls int + lifecycleInspects int + lifecycleMutations int } func (s *stubTypedContainerUpdater) TypedContainerUpdate(context.Context, string, string, string, func(string)) (agentexec.DockerContainerUpdateOutcome, error) { @@ -2308,6 +2310,16 @@ func (s *stubTypedContainerUpdater) TypedContainerUpdate(context.Context, string return agentexec.DockerContainerUpdateOutcome{Success: true}, nil } +func (s *stubTypedContainerUpdater) InspectDockerContainerLifecycle(context.Context, string, string) (agentexec.DockerContainerLifecycleSnapshot, error) { + s.lifecycleInspects++ + return agentexec.DockerContainerLifecycleSnapshot{ContainerID: strings.Repeat("a", 64), State: "running", Running: true}, nil +} + +func (s *stubTypedContainerUpdater) MutateDockerContainerLifecycle(context.Context, string, string, string) error { + s.lifecycleMutations++ + return nil +} + func TestLateBoundDockerUpdaterBridgesModuleWhenItComesUp(t *testing.T) { bridge := &lateBoundDockerUpdater{} @@ -2328,12 +2340,22 @@ func TestLateBoundDockerUpdaterBridgesModuleWhenItComesUp(t *testing.T) { if stub.calls != 1 { t.Fatalf("expected one delegated call, got %d", stub.calls) } + if _, err := bridge.InspectDockerContainerLifecycle(context.Background(), "docker", strings.Repeat("a", 64)); err != nil { + t.Fatalf("bridge lifecycle inspect refused: %v", err) + } + if err := bridge.MutateDockerContainerLifecycle(context.Background(), "docker", "restart", strings.Repeat("a", 64)); err != nil { + t.Fatalf("bridge lifecycle mutation refused: %v", err) + } + if stub.lifecycleInspects != 1 || stub.lifecycleMutations != 1 { + t.Fatalf("lifecycle calls = inspect %d mutate %d", stub.lifecycleInspects, stub.lifecycleMutations) + } } func TestDockerAgentImplementsTypedContainerUpdater(t *testing.T) { // The bridge installs by structural assertion; if the Docker module's // method signature drifts, updates silently refuse at runtime. Pin it. var _ hostagent.DockerContainerUpdater = (*dockeragent.Agent)(nil) + var _ hostagent.DockerContainerLifecycleOperator = (*dockeragent.Agent)(nil) } func TestAllowPlaintextHTTPFlagParsesAndDefaultsClosed(t *testing.T) { diff --git a/docs/AI_AUTONOMY.md b/docs/AI_AUTONOMY.md index b5610ffe6..66f2edff5 100644 --- a/docs/AI_AUTONOMY.md +++ b/docs/AI_AUTONOMY.md @@ -105,7 +105,7 @@ When Patrol mode is `approval`, `assisted`, or `full`, Patrol investigates findi | Setting | Default | Range | Description | |---------|---------|-------|-------------| -| `patrol_investigation_budget` | 15 | 5–30 | Maximum evidence-tool calls per investigation; Patrol derives a separate model-response safety ceiling | +| `patrol_investigation_budget` | 10 | 5–30 | Maximum evidence-tool calls per investigation; Patrol derives a separate model-response safety ceiling | | `patrol_investigation_timeout_sec` | 600 | 60–1800 | Maximum seconds per investigation | | `max_concurrent_investigations` | 3 | — | Parallel investigation limit | | `max_attempts_per_finding` | 3 | — | Retries before marking as `needs_attention` | diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 3c2768bbd..61066e70e 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -835,6 +835,16 @@ lifecycle/update chronology within its bounded skew window and then binds a slightly future valid observation to the server receipt boundary for canonical evidence. Agent lifecycle consumers must not reject that bounded case, widen the skew window, or clamp stale/excessively future evidence into validity. +Inside a containerized Unified Agent, the closed start/stop/restart command +must inspect and mutate through the Docker / Podman module's already-connected +daemon API; it must not depend on a second `docker` or `podman` executable being +present in the agent image. The host-command bridge exposes only exact-container +inspect plus the allowlisted start, stop, and restart verbs. Execution remains +read-before-mutate-read, and a mutation request is sent exactly once: an +ambiguous daemon transport failure must surface for receipt reconciliation +rather than being retried and possibly applying the operation twice. Native +host agents may retain the equivalent CLI implementation as a compatibility +fallback when no module bridge is configured. Docker / Podman container image updates are a fourth typed operation on that same closed channel, not an extension of the raw command path and not a diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index f325c506d..74b11ffa5 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -429,7 +429,28 @@ and later Patrol seed context omit artifact contents. The detection profile permits this Pulse-state proposal write explicitly; interactive Assistant and Patrol investigation profiles do not. A successful proposal remains `uncovered/observer_proposed`: this tool has no validator, installer, execution, -health-lease, or infrastructure-action authority. +health-lease, or infrastructure-action authority. It remains a governed write +for invocation policy, but its returned revision and observer identity are the +authoritative persisted Pulse-state record; like Patrol finding-lifecycle +writes, it neither enters nor satisfies the infrastructure read-after-write +FSM. This separation prevents a non-executable proposal from trapping a Watch +run in a verification loop while preserving mandatory verification after any +real infrastructure mutation. +In the detection profile, a scoped objective-planning run may execute +`patrol_propose_observer` directly from `RESOLVING` because core supplied the +exact objective identity and current optimistic revision and the store +revalidates both atomically. That exception does not apply to finding writes, +interactive or investigation profiles, or `VERIFYING`; an observer proposal +can never bypass verification owed by a real infrastructure mutation. +The proposal also carries a closed `evidence_fit` classification. `direct` +means the predicate itself measures the full retained outcome; `proxy` means it +is a useful correlated wake signal only. Core validates, installs, evaluates, +and leases both safe signal types, but `derivePatrolObjectiveCoverage` permits +only a healthy direct observer to become `covered`. A healthy proxy remains +`uncovered/observer_proxy`, and a missing fit on an older stored observer is +normalized conservatively to proxy. Semantic adequacy therefore stays +model-authored and improves with the model, while the deterministic core owns +the fail-safe rule that uncertainty cannot become a protection claim. An observer wake must preserve the exact retained objective rather than degrading it into a generic resource alert. `PatrolScope.ObjectiveContext` @@ -825,14 +846,18 @@ read-only chat demotes Patrol or infrastructure action policy. The manual Patrol route is an extension boundary for scoped work. `POST /api/ai/patrol/run` (`HandleForcePatrol`) accepts an optional scope body (`resource_ids` and/or `resource_types`, plus optional `alert_identifier`, -`alert_type`, and `context`) to run a manual Targeted check through the same -`TriggerScopedPatrol` engine and scoped run record as automatic alert-triggered -work; with no body it keeps the fleet-wide Patrol check. The scoped path must +and `alert_type`) to run a manual Targeted check through the same scoped engine +and run record as automatic alert-triggered work; with no body it keeps the +fleet-wide Patrol check. Client-authored context is rejected. The scoped path must reuse the existing scoped engine rather than adding a parallel trigger route, honour the same Patrol readiness gate as a full run, bypass the full-run cadence gate (targeted checks never consume a manual full-run allowance), and carry resource identity only — no command, prompt, or remediation payload — while the route keeps requiring admin plus `ai:execute` scope for both shapes. +Manual scoped admission atomically reserves the single Patrol slot and returns +its run identity before the provider goroutine starts. If another run owns the +slot it returns an honest typed conflict; automatic scoped triggers retain +their bounded internal requeue path. Pulse Intelligence presents Patrol as the primary first-party operations surface. Pulse Assistant is the in-app contextual explanation, approval-card, @@ -3317,6 +3342,11 @@ Qualification floor: Patrol model launch and product-claim qualification must us and the catalog response plus captured proposal must carry that canonical target. Unknown or ambiguous references still fail closed. The model is not responsible for Pulse's provider-to-canonical identity plumbing. + The proposal canonicalizer accepts either that provider coordinate or the + equivalent exact Pulse read coordinate + `app-container::` when it uniquely identifies + the same unified resource. It does not accept a partial ID, display name, or + ambiguous host/container pairing. Agentic context compaction is also evidence-preserving rather than merely size-reducing. When a read tool returns structured safety-relevant state, its deterministic knowledge projection must retain the bounded fields needed @@ -3524,6 +3554,15 @@ resolve canonical/source IDs and unique aliases before collection, reject provenance — the `source` that produced it and its `confidence` — alongside the fact's category/key/value, so the model can attribute and weight what it reports instead of stating untraceable facts. Both are omitted when empty. + A targetless `action=get` (including the common `{action:get, limit:N}` + form) is a bounded alias for `action=list`; because it requests no specific + resource, it must not fail merely on missing resource type. Any partially + specified get remains the strict single-resource operation. An exact + canonical `app-container` resource ID is itself a complete coordinate: the + runtime may derive its target and provider container ID only from the unique + unified-resource record. Names, aliases, prefixes, provider IDs, and + ambiguous or unknown canonical IDs never authorize that inference and fail + closed until an explicit target is supplied. When the shared registry blocks a control tool in read-only mode, its operator guidance must point to Pulse Intelligence > Provider & Models settings and the Pulse Assistant Permissions Control mode, not legacy Pulse @@ -4101,6 +4140,9 @@ model responses, model-selected tool calls, and evidence-tool calls. The operator-facing investigation budget limits evidence calls; core derives a separate model-response ceiling with reserved completion capacity, and the terminal `patrol_propose_action` call does not consume evidence budget. The +default budget is ten evidence calls: enough for model-led diagnosis while +making the shipped qualification ceiling the ordinary product posture instead +of a lab-only override. Operators may still choose the bounded 5–30 range. The investigation execution profile injects an evidence-completion checkpoint, then structurally removes evidence tools at exhaustion while retaining only the typed proposal route and final prose. This remains model-led: Pulse defines the @@ -6301,6 +6343,17 @@ requires one match, but scoring must not fail an otherwise exact causal diagnosis merely because the model used the provider-visible equivalent rather than the injector's verb. These alternatives are fixed in the manifest before execution and never inferred from the model response or its tool path. +An autonomous remediation scenario uses the explicit `await_autonomous` +decision. It requires effective Full autonomy (the manifest's legacy +`autonomous` spelling aliases to `full`), and the qualification client sends no +approval decision and no execute request. It waits for the independently +created Patrol action to reach its governed terminal lifecycle, then checks the +required lifecycle-verification state and independent postconditions. The +separate operator authorization to run a disposable fault scenario permits the +fault injection only; it is not action approval. Only this exact autonomous +shape may allow the declared fault to be repaired before the post-Patrol +observation, while unexpected mutation and inventory-restoration gates remain +mandatory. The canonical `pulse_query` schema and executor must admit the same resource types. In particular, `action=get` accepts `docker-host`, returns a governed read-only host response when the identity resolves, and returns a successful @@ -6342,6 +6395,12 @@ verification step is a valid recommendation when remediation is not yet justified; impact remains optional so the contract never pressures the model to fabricate a consequence. Providers that omit either grounding field must receive a tool error and no partial finding may be persisted. +During a first-party Patrol run, the general model-facing +`pulse_alerts(action=findings)` spelling is a scoped alias for the same active +finding snapshot as `patrol_get_findings` and satisfies the duplicate-check +precondition. The adapter may return only findings inside the run's effective +resource scope, never dismissed history or out-of-scope records. This keeps +ordinary model tool choice composable without weakening the lifecycle gate. The provider schema is also the source of truth for the required-argument checklist rendered into Patrol's normal and bounded final-decision prompts. Every report call must be independently complete, including parallel calls for @@ -6893,7 +6952,7 @@ resource, record, evidence, actor, or provider-instance identity. ### Trust-gate manual Patrol acceptance -An unscoped manual Patrol run is accepted only after `internal/ai/patrol.go` +An unscoped or scoped manual Patrol run is accepted only after the runtime atomically reserves one execution slot and assigns the run ID and start time. That accepted identity is synchronously visible through Patrol status before the provider goroutine can emit its first event. A concurrent request therefore diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 448f6ec28..8327b7860 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -63,6 +63,14 @@ server-derived `coverage` and the internal observer lifecycle for honest status, but public request schemas do not accept either field. Unknown fields, trailing JSON, over-limit bodies, unsupported statuses, and malformed scope entries fail closed. Objective text is never copied into audit messages. +Observer responses include the additive closed `evidence_fit` value, `direct` +or `proxy`. Only a healthy installed direct observer may produce +`coverage.state=covered`; a healthy proxy remains +`uncovered/observer_proxy` with a server-authored explanation. Missing fit on +older persisted observers is interpreted as proxy, so an upgrade cannot turn a +weak correlated signal into an implicit protection claim. The model-facing +proposal tool requires the fit, while objective create/update clients still +cannot author it. Objective-triggered Patrol run and finding payloads retain an optional `objective_context` snapshot containing the exact objective/observer identity, revision, bounded desired-outcome text, affected canonical resources, local @@ -1984,6 +1992,8 @@ coverage and observer artifacts remain server-owned. Successful active create or material update requests immediately ask the tenant Patrol service to plan coverage, while the HTTP response remains the durable objective truth and does not claim that queue acceptance means coverage. +The frontend mirror treats `observer_proxy` as uncovered objective truth and +may present its useful local signal without translating it into covered state. The route-backed Actions review consumes an exact typed action id through its browser query state and resolves lifecycle state from `GET /api/actions/{id}`; Patrol may link to that identity but must not infer Open versus History from @@ -3262,8 +3272,14 @@ snapshot before acknowledging the run. Unmatched, ambiguous, or zero-match explicit identities return the stable `patrol_scope_unresolved` 422 envelope; accepted responses include the requested-to-effective scope resolution. The runtime must re-resolve at execution time and persist a failed run if collection -drift turns the accepted scope into a zero-match race. Clients must not infer a -successful targeted check from a queued response alone. +drift turns the accepted scope into a zero-match race. Admission atomically +reserves the same single execution slot used by unscoped runs before returning +HTTP success. The response therefore includes `run_id` and `started_at`; a busy +slot returns the typed `409 patrol_already_running` envelope instead of +acknowledging work that may be dropped. Qualification clients may retry that +honest conflict within their scenario timeout. Clients must not infer a +successful targeted check from admission alone: the named run record remains +the authoritative analysis outcome. 1. Update contract tests when payloads change, including admin verification endpoints such as `POST /api/ai/patrol/preflight` whose response shape (`tool_call_observed`, `duration_ms`, classified `cause`/`summary`/`recommendation`, plus `recorded_at`/`recorded_at_unix` for the cached snapshot) is part of the canonical Patrol diagnostic surface, the `patrol_preflight` snapshot field on `/api/settings/ai` that hydrates the Check Patrol model panel on page load, the auto-trigger contract on `POST/PUT /api/settings/ai` whose handler dispatches preflight in the background only when the change actually moved Patrol transport so routine saves do not write a new `patrol_preflight` snapshot, the startup-seed contract where `NewAISettingsHandler` dispatches the same async preflight after `LoadConfig()` succeeds so the first `/api/settings/ai` poll after a Pulse restart already carries a populated `patrol_preflight` snapshot, and the GET-symmetry contract where `HandleGetAISettings` includes `patrol_readiness` (with the cached-preflight-augmented `tools` check) on the same response that already carries `patrol_preflight`, so the Patrol page picks up classified preflight evidence on first load instead of only after a save; readiness checks may keep stable machine IDs such as `configuration`, but user-facing labels in this payload must say Patrol mode rather than Patrol configuration, and the settings UI must summarize successful diagnostic snapshots as model readiness instead of rendering raw preflight/tool-call wording The same diagnostic payload may inform Patrol page setup banners, but the @@ -4111,7 +4127,9 @@ separately, so API, audit, qualification, and enterprise consumers no longer infer provider turns from tool events. The existing `investigation_budget` settings payload remains wire-compatible but is now defined as an evidence-call budget; the server derives the model-response safety ceiling and reserves -terminal proposal/final-summary capacity. +terminal proposal/final-summary capacity. An absent or zero persisted setting +uses the shipped ten-call default; explicit values retain the bounded 5–30 +contract. Unified Agent connections now carry agent-authored lifecycle evidence through the shared host model and `/api/connections` contract. The payload may include @@ -8657,6 +8675,15 @@ version/status metadata without rewriting historical evidence. Stable failure co missing, stale, wrong-organization, wrong-actor, expired, revoked, malformed, conflicting, and unavailable-store evidence. +Paid adapters may return a compact mutation acknowledgement from `PUT`; it is +not the canonical effective-state projection. First-party clients must follow +every successful autonomy write with `GET /api/ai/patrol/autonomy` and render +that read, including license clamps, unlock state, acknowledgement status, and +server-derived compatibility fields. They must never dereference optional +fields from the compact write response or optimistically claim Autopilot is +active. Consumers treat an absent, malformed, or Go zero-time acknowledgement +`expiresAt` as unbounded/no expiry rather than displaying a year-one date. + Task 09 APT executors consume Task 10 `ActionResultV2` without local truth enums. Host-update `MutationStarted` means the fixed install command began; metadata refresh, refreshed-inventory drift, zero-pending state, and pre-install @@ -8820,9 +8847,12 @@ must also keep a transport/network failure distinct from a structured backend rejection. Scoped targeted checks retain their established scope-resolution response and -do not claim the unscoped run-slot acceptance fields. All remediation remains -on the canonical Actions API and lifecycle; accepting Patrol analysis does not -authorize an alternate execution route. +now share the same atomic `run_id` / `started_at` admission fields and busy-slot +conflict as unscoped checks. Background alert, anomaly, and objective triggers +continue through their bounded internal requeue policy; only explicit manual +and qualification requests use the synchronous admission contract. All +remediation remains on the canonical Actions API and lifecycle; accepting +Patrol analysis does not authorize an alternate execution route. `internal/api/ai_handlers_patrol_actions_additional_test.go` proves the acceptance and typed-conflict payloads, while `frontend-modern/src/features/patrol/__tests__/patrolRunAcceptance.test.ts` diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index c022dbb46..519c56d56 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2669,6 +2669,12 @@ Patrol-only overlay, selector, badge vocabulary, or focus model. The modal keeps the standard backdrop, Escape, focus trap/return, bounded viewport, scrolling, and responsive footer behavior while the feature owns only objective-specific copy and orchestration. +The same shared metadata badge renders a healthy server-owned proxy observer as +`Useful signal only` with warning tone. The row keeps the backend explanation +that the signal does not directly measure the full objective, while only +server-authored `covered` state may render `Watching in background`. This is a +presentation of the canonical objective contract, not a frontend inference or +a new badge primitive. ### System member rows are source-type aware @@ -5753,6 +5759,13 @@ backend rejection remains distinct from a browser or network failure. Any action proposed from that run still hands off to the shared canonical Actions review instead of adding approve, execute, or retry-mutation controls to the Patrol feature. +Patrol autonomy controls follow the same server-truth discipline: after any +successful autonomy PUT, the feature reloads the canonical GET projection +before rendering the selected mode. Compact paid-runtime acknowledgements are +not frontend state and missing nested fields in them must not crash the page. +An absent, malformed, or Go zero-time `expiresAt` value is rendered as no +expiry; it must never become a year-one locale date. A real bounded future +expiry remains visible beside the acknowledgement status. Storage detail primitives render physical-disk collection truth explicitly: temporarily unavailable, provider/controller unsupported, and unexpectedly diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index 0764ddc98..abcad79f7 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -1599,6 +1599,14 @@ and `internal/dockeragent/swarm.go` must keep Pulse's package-local implementation routes through maintained `github.com/moby/moby/api` and `github.com/moby/moby/client` modules, so monitoring runtime collection does not drift back onto the legacy `github.com/docker/docker` Go module line. +The Unified Agent may share that maintained, already-connected client through +a narrow typed lifecycle bridge, but collection remains read-only unless the +canonical governed command channel invokes the bridge. The bridge may inspect +one exact container and issue one allowlisted start, stop, or restart; it is not +a general monitoring mutation API, does not expose the daemon client to model +tools, and must not retry an ambiguous mutating request. Module absence or a +runtime mismatch fails closed while ordinary inventory collection continues +under its existing local configuration and privacy controls. That same monitoring owner now also governs restart-safe standalone host continuity for monitored-system grouping. `internal/monitoring/monitor_agents.go` must persist recent host identity at report time, and diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 4fc4cd9af..c33eec403 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -173,8 +173,9 @@ The canonical retained-intent, proposal, and first local execution slices are im They provide encrypted retained intent, optimistic revisions, resource scoping, model seed projection, the core-owned observer state machine, and the Patrol-detection-only `patrol_propose_observer` builder. The model supplies a -bounded canonical-JSON probe proposal, measurable interpretation, wake evidence, -declared requirements, and one trigger kind; core supplies identity, version, +bounded canonical-JSON probe proposal, measurable interpretation, `direct` or +`proxy` evidence fit, wake evidence, declared requirements, and one trigger +kind; core supplies identity, version, read-only posture, digest, encrypted persistence, and the `proposed` state. The artifact is excluded from public objective reads and later prompt seeds. The public API cannot attach an observer or author coverage, and the model-facing @@ -207,13 +208,24 @@ outcome remain separate: a healthy observer may report that its objective is currently breached. Editing the retained brief, optional context, or resource scope disables the existing observer and clears its lease, because an artifact validated against old intent cannot remain proof of coverage for new intent. +Observer health and semantic fit are also separate. `direct` means the predicate +itself measures the full retained outcome; `proxy` means the signal is useful +for waking Patrol but only correlates with that outcome. Core installs and +leases either safe local signal, but a healthy proxy remains +`uncovered/observer_proxy` instead of becoming a false protection claim. Missing +fit on older stored observers defaults conservatively to proxy. This lets the +model exploit cheap reachability or resource-state signals without pretending +they prove richer outcomes such as smooth playback or successful recording. Creating or materially updating an active objective queues an immediate, objective-identity-deduplicated coverage-planning Patrol run. The first-party `PatrolObjectivesPanel` exposes one outcome statement, optional context and optional resource scope, then shows the server-owned covered, degraded, or uncovered truth with pause, resume, edit, and delete controls. Saving text is -never presented as equivalent to active protection. +never presented as equivalent to active protection. A healthy proxy uses the +plain-language `Useful signal only` badge and the server explanation that the +full objective is not directly measured; it must not render as `Watching in +background` or count toward protected outcomes. Unsupported trigger or probe designs transition to `rejected` with an explicit machine validation reason and uncovered coverage. A rejected or degraded @@ -568,6 +580,10 @@ attention`, `approval needed`, `outcome verified`, `no active work`) instead as automatic alert-triggered work (governed by the `ai-runtime` manual Patrol route contract), so the operator still sees a `Targeted check` rather than a route-specific label. + That request uses synchronous admission rather than the background trigger + queue. Success carries a named run; if Patrol is busy the caller receives an + explicit already-running conflict and may retry, so a Targeted check can + never display accepted while its work was actually dropped. Active Patrol finding expansion must stay action-led: description, impact, recurrence summary, primary action, Assistant handoff, approval, and manual controls are acceptable default content, but raw lifecycle telemetry belongs @@ -2313,6 +2329,13 @@ authoritative. A provider/runtime error record remains a completed accepted run outcome. Missing data, refresh failure, backend rejection, and browser/network failure remain separate states and copy. +Autonomy writes use the complementary authoritative-read rule. A successful +`PUT /api/ai/patrol/autonomy` may be only a compact paid-runtime +acknowledgement, so `usePatrolIntelligenceState.ts` always reloads +`GET /api/ai/patrol/autonomy` before updating visible control state. The page +must not read optional nested autonomy fields from the write response or claim +Autopilot is active before the server-derived effective mode is loaded. + Reconciliation is generation-aware and cancellable. Route changes, retries, or superseding starts cancel the old timer; stale completions cannot overwrite the current run, and reconciliation never starts a replacement provider call. diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 3d9d580dc..b7a577c1d 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -1501,6 +1501,14 @@ handoff: the executor may use agent command execution only after scope, approval/policy, stale-plan, operator-lock, source-freshness, and runtime posture checks pass, and it must record redacted audit and verification facts instead of exposing raw command text through monitoring-readable surfaces. +The containerized Unified Agent's daemon bridge does not widen that grant. It +accepts only the canonical typed lifecycle payload after command admission, +binds an immutable container id and an allowlisted start/stop/restart operation, +and exposes no raw daemon request, command text, arbitrary name, or general +socket capability to the model or server. It reuses the locally configured +module connection that already collects Docker / Podman inventory; a missing +or mismatched module fails before mutation, and an ambiguous mutating daemon +call is never retried automatically. Proxmox VM/LXC lifecycle execution is governed by the same privileged action handoff: `start`, `shutdown`, `reboot`, and `stop` may use a Proxmox node command agent only after the API action scope, approval/policy, stale-plan, diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 086d4dda8..2671e6e23 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -2285,6 +2285,11 @@ evidence-call and model-response budgets/counters. Storage and recovery may use those values only as investigation cost/load evidence; they do not establish storage health, backup completion, recovery-point validity, restore authority, or post-action verification. +That adjacent handler also admits scoped and fleet-wide manual Patrol work +through one atomic runtime slot and returns the accepted run identity or a +typed already-running conflict. Storage and recovery may supply a resource as +investigation scope, but admission is not storage mutation authority, backup +success, recovery verification, or an alternate recovery queue. ### Canonical mutation-plane dependency diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 3240e6ef1..3a872a988 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,29 +1,27 @@ { "version": 1, - "base_sha": "e5cad4c2c34529c0b15d8c32b8d1e53d0ddd84ad", - "verified_at": "2026-08-14T08:26:04Z", + "base_sha": "772ff3087d60ac9ee9b69ddf42bcc76a8eae5be0", + "verified_at": "2026-08-14T11:45:19Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx", + "frontend-modern/src/components/shared/useDialogState.ts", "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx", - "frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx", "frontend-modern/src/features/patrol/PatrolObjectivesPanel.tsx", - "frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx", - "frontend-modern/src/features/patrol/patrolHomePresentation.ts" + "frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts" ], "content_sha256": { - "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "01f1010536c0b1551a09fa5634c9714cb771f10a37b274ae068aa1bbd3860b59", - "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx": "41aa54fdf6d23ceb0019455e2a112b47d0164cf2e1dd36abc45f8faf47bc77e9", - "frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx": "780a27fcab535413b8446c5ec89637f98c08af1d8b57f77157028e499ad6b4d4", - "frontend-modern/src/features/patrol/PatrolObjectivesPanel.tsx": "8a68062a24352ac46384030bd267f1143572588ff36f061ec0cbed3c03555da4", - "frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx": "6d2cf91943e49db10cd94c6639390297ddaff170ebf136b6c48e991cd14c134d", - "frontend-modern/src/features/patrol/patrolHomePresentation.ts": "18799e39f1bd4cdeaf93e494f4b84fc898aef356b111b030687885cbfdbd478b" + "frontend-modern/src/components/shared/useDialogState.ts": "3566700d8a8e72f3962204f12096912b84fb3bf089a4b181c1d9326ba85a1306", + "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx": "272c6cb9c975cb346aa96082e804ad2eb08419d7ab2157429b901ba8b2f67a94", + "frontend-modern/src/features/patrol/PatrolObjectivesPanel.tsx": "ba86e9637aca3abd63de515d5e98a8881ddfc4ed7b308aaa651926fa5eb48db9", + "frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts": "24ac4a67d8e97bc39867238c34b2cf4deede69ccf671eef1ffc47c739a25db4a" }, - "routes": ["/patrol"], + "routes": [ + "/patrol" + ], "viewports": [ { - "width": 1440, - "height": 1000 + "width": 1280, + "height": 800 }, { "width": 390, @@ -31,22 +29,21 @@ } ], "states": [ - "watch-only background posture with effective autonomy consequence and no commercial upgrade action", - "empty objective list with honest zero-protection summary", - "ten genuine user decisions compacted to five initial rows", - "verified-receipt empty state without implying successful work", - "autonomy disclosure expanded", - "objective creation dialog open and dismissed", - "typed attention detail open and closed with evidence, protection, and timeline", - "desktop and narrow layouts without horizontal overflow" + "Watch only with direct objective shown as Watching in background", + "Watch only with Jellyfin and Frigate proxy objectives shown as Useful signal only", + "objective creation dialog open with the outcome textarea focused", + "Autopilot acknowledgement modal before and after acknowledgement checkbox", + "Autopilot active with canonical server state and no year-one expiry", + "Autopilot revoked and Patrol returned to Watch only", + "narrow Patrol layout with no horizontal overflow" ], "interactions": [ - "expanded the secondary How Patrol operates disclosure and confirmed Watch only without Plans & Billing", - "opened the add-objective dialog, inspected the outcome-first form, cancelled it, and dismissed it with Escape", - "confirmed objective-dialog focus returned to Add objective after Escape", - "opened the deepest attention detail and inspected lifecycle, evidence, protection, timeline, and resource actions", - "closed attention detail and confirmed focus returned to the originating decision row", - "expanded all ten decisions, collapsed back to five, and confirmed the compact queue counts", - "verified document width matched both 1440px and 390px viewports" + "opened Add objective and confirmed autofocus", + "closed objective dialog with Escape and confirmed focus return", + "opened Autopilot acknowledgement and enabled activation with the acknowledgement checkbox", + "activated Autopilot through the real UI and confirmed the server-derived mode", + "revoked Autopilot through the real UI", + "switched Patrol back to Watch only", + "resized to 390 by 844 and confirmed body and document scroll widths stayed at 390" ] } diff --git a/frontend-modern/public/docs/AI_AUTONOMY.md b/frontend-modern/public/docs/AI_AUTONOMY.md index b5610ffe6..66f2edff5 100644 --- a/frontend-modern/public/docs/AI_AUTONOMY.md +++ b/frontend-modern/public/docs/AI_AUTONOMY.md @@ -105,7 +105,7 @@ When Patrol mode is `approval`, `assisted`, or `full`, Patrol investigates findi | Setting | Default | Range | Description | |---------|---------|-------|-------------| -| `patrol_investigation_budget` | 15 | 5–30 | Maximum evidence-tool calls per investigation; Patrol derives a separate model-response safety ceiling | +| `patrol_investigation_budget` | 10 | 5–30 | Maximum evidence-tool calls per investigation; Patrol derives a separate model-response safety ceiling | | `patrol_investigation_timeout_sec` | 600 | 60–1800 | Maximum seconds per investigation | | `max_concurrent_investigations` | 3 | — | Parallel investigation limit | | `max_attempts_per_finding` | 3 | — | Retries before marking as `needs_attention` | diff --git a/frontend-modern/src/components/shared/__tests__/Dialog.test.tsx b/frontend-modern/src/components/shared/__tests__/Dialog.test.tsx index aaa0d3c43..1b5f0f60c 100644 --- a/frontend-modern/src/components/shared/__tests__/Dialog.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/Dialog.test.tsx @@ -110,4 +110,18 @@ describe('Dialog', () => { fireEvent.keyDown(document, { key: 'Tab', shiftKey: true }); expect(last).toHaveFocus(); }); + + it('honors an explicitly requested initial focus target', async () => { + render(() => ( + undefined}> +
+ +