diff --git a/README.md b/README.md index 47e2430b8..5355b8c42 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ Self-hosted tiers: |---|---:|---|---:|---| | Community | Free | Included | 7 days | Full self-hosted monitoring | | Relay | $39/yr or $4.99/mo | Included | 14 days | Remote web access, mobile app pairing, and push notifications | -| Pro | $79/yr or $8.99/mo | Included | 90 days | Root-cause analysis, safe remediation workflows, and operations tooling | +| Pro | $79/yr or $8.99/mo | Included | 90 days | Hands-on Patrol modes, issue investigation, verified fixes, and operations tooling | Pulse still counts top-level monitored systems once no matter how they are collected. VMs, containers, pods, disks, backups, and other child resources @@ -198,8 +198,8 @@ Runtime-aligned capability summary: |---|:---:|:---:|:---:|:---:| | Pulse Patrol (Background Health Checks) | ✅ | ✅ | ✅ | ✅ | | Remote Access / Mobile / Push | — | ✅ | ✅ | ✅ | -| Alert-Triggered Root-Cause Analysis | — | — | ✅ | ✅ | -| Safe Remediation Workflows | — | — | ✅ | ✅ | +| Patrol Investigates Issues | — | — | ✅ | ✅ | +| Patrol Handles Safe Fixes | — | — | ✅ | ✅ | | Centralized Agent Profiles | — | — | ✅ | ✅ | | Update Alerts (Container/Package Updates) | ✅ | ✅ | ✅ | ✅ | | SSO (OIDC/SAML/Multi-Provider) | ✅ | ✅ | ✅ | ✅ | diff --git a/cmd/agent-probe/main.go b/cmd/agent-probe/main.go index 93c8512c7..e845d9792 100644 --- a/cmd/agent-probe/main.go +++ b/cmd/agent-probe/main.go @@ -25,8 +25,11 @@ // // Hard constraints honored on purpose: // -// - Standard library only. No internal/ai imports, no Pulse types -// reused. A real external agent has no privileged access. +// - No internal/ai or API handler imports. The only Pulse package +// reused is the tiny agentcapabilities wire/projection package, so this +// in-repo reference client cannot drift from the manifest shape or path +// projection rules. A real external agent can define the same JSON shape +// from the manifest or a generated client. // - Branches on stable error codes from the manifest, never on // human-readable error messages. // - Resolves paths from the manifest rather than hardcoding them. @@ -44,40 +47,18 @@ package main import ( - "bufio" "context" "encoding/json" "flag" "fmt" - "io" "net/http" - "net/url" "os" "strings" "time" + + "github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities" ) -// agentCapability mirrors the api package's AgentCapability wire -// shape — defined inline so this program depends on nothing in the -// pulse module. If the manifest's shape evolves, this struct -// follows; the JSON tags are the contract. -type agentCapability struct { - Name string `json:"name"` - Description string `json:"description"` - Category string `json:"category"` - Method string `json:"method"` - Path string `json:"path"` - Scope string `json:"scope"` - ResponseShape string `json:"responseShape,omitempty"` - ErrorCodes []string `json:"errorCodes,omitempty"` - RequestBodyShape string `json:"requestBodyShape,omitempty"` -} - -type agentCapabilitiesManifest struct { - Version string `json:"version"` - Capabilities []agentCapability `json:"capabilities"` -} - // fleetResource is the per-resource thin rollup the fleet endpoint // returns. We only depend on the fields the probe actually needs to // pick a focus and print one-line summaries. @@ -102,15 +83,6 @@ type fleetContext struct { GeneratedAt time.Time `json:"generatedAt"` } -// errorEnvelope is the shared shape every agent-surface endpoint -// uses on failure. The "error" field is a stable snake_case code -// (e.g. resource_not_found, operator_state_not_set); "message" is -// human text agents may surface but must not branch on. -type errorEnvelope struct { - Error string `json:"error"` - Message string `json:"message"` -} - func main() { baseURL := flag.String("base-url", "http://localhost:7655", "Pulse base URL") token := flag.String("token", "", "API token with monitoring:read scope (required for triage and depth steps)") @@ -125,7 +97,7 @@ func main() { client := &http.Client{Timeout: 15 * time.Second} // --- 1. Discovery. Public, no token needed. --- - manifest, err := fetchManifest(client, *baseURL) + manifest, err := agentcapabilities.FetchManifest(context.Background(), client, *baseURL) if err != nil { exitf("discovery failed: %v", err) } @@ -134,26 +106,13 @@ func main() { fmt.Printf(" %-22s %s %s (%s)\n", cap.Name, cap.Method, cap.Path, cap.Scope) } - byName := map[string]agentCapability{} - for _, c := range manifest.Capabilities { - byName[c.Name] = c - } - - fleetCap, ok := byName["get_fleet_context"] - if !ok { - exitf("manifest missing required capability get_fleet_context") - } - contextCap, ok := byName["get_resource_context"] - if !ok { - exitf("manifest missing required capability get_resource_context") - } - streamCap, ok := byName["subscribe_events"] + streamCap, ok := agentcapabilities.FindCapability(manifest.Capabilities, agentcapabilities.EventSubscriptionCapabilityName) if !ok { exitf("manifest missing required capability subscribe_events") } // --- 2. Triage. Authenticated. --- - fleet, err := fetchFleet(client, *baseURL, fleetCap.Path, *token) + fleet, err := fetchFleet(context.Background(), client, *baseURL, manifest.Capabilities, *token) if err != nil { exitf("triage failed: %v", err) } @@ -184,8 +143,17 @@ func main() { fmt.Println("\nno resources visible to this token; skipping depth step") } else { // --- 3. Depth. --- - depthPath := strings.Replace(contextCap.Path, "{resourceId}", focus.CanonicalID, 1) - body, err := fetchAuthenticatedRaw(client, *baseURL+depthPath, *token) + body, err := agentcapabilities.CallRequestResponseCapabilityHTTPBodyByName( + context.Background(), + client, + *baseURL, + *token, + manifest.Capabilities, + agentcapabilities.ResourceContextCapabilityName, + map[string]any{ + "resourceId": focus.CanonicalID, + }, + ) if err != nil { exitf("depth failed for %s: %v", focus.CanonicalID, err) } @@ -207,7 +175,7 @@ func main() { streamCap.Path, *eventTimeout) ctx, cancel := context.WithTimeout(context.Background(), *eventTimeout) defer cancel() - if err := readOneSSEEvent(ctx, *baseURL+streamCap.Path, *token); err != nil { + if err := readOneSSEEvent(ctx, *baseURL, streamCap.Path, *token); err != nil { // Timeout is the boring case (no events fired); not a hard // failure for a probe. if strings.Contains(err.Error(), "deadline exceeded") { @@ -219,24 +187,16 @@ func main() { fmt.Println("\nagent-probe done — discovery, triage, depth, and push all walked the substrate cleanly.") } -func fetchManifest(client *http.Client, baseURL string) (*agentCapabilitiesManifest, error) { - resp, err := client.Get(baseURL + "/api/agent/capabilities") - if err != nil { - return nil, fmt.Errorf("GET capabilities: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("GET capabilities: status %d", resp.StatusCode) - } - var manifest agentCapabilitiesManifest - if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil { - return nil, fmt.Errorf("decode manifest: %w", err) - } - return &manifest, nil -} - -func fetchFleet(client *http.Client, baseURL, path, token string) (*fleetContext, error) { - body, err := fetchAuthenticatedRaw(client, baseURL+path, token) +func fetchFleet(ctx context.Context, client *http.Client, baseURL string, capabilities []agentcapabilities.Capability, token string) (*fleetContext, error) { + body, err := agentcapabilities.CallRequestResponseCapabilityHTTPBodyByName( + ctx, + client, + baseURL, + token, + capabilities, + agentcapabilities.FleetContextCapabilityName, + nil, + ) if err != nil { return nil, err } @@ -247,43 +207,6 @@ func fetchFleet(client *http.Client, baseURL, path, token string) (*fleetContext return &fleet, nil } -// fetchAuthenticatedRaw is the shared GET helper for any -// authenticated agent-surface endpoint. Branches on the stable -// error envelope when a non-2xx comes back so the caller sees the -// agent-stable code, not just an HTTP status. -func fetchAuthenticatedRaw(client *http.Client, fullURL, token string) ([]byte, error) { - parsed, err := url.Parse(fullURL) - if err != nil { - return nil, fmt.Errorf("parse %q: %w", fullURL, err) - } - req, err := http.NewRequest(http.MethodGet, parsed.String(), nil) - if err != nil { - return nil, fmt.Errorf("build request: %w", err) - } - req.Header.Set("X-API-Token", token) - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("GET %s: %w", parsed.Path, err) - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("read body: %w", err) - } - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return body, nil - } - // Non-2xx: try to surface the stable error code from the - // envelope. Unauthenticated rejection comes through as plain - // text from the auth middleware (not the envelope), so fall back - // to the body verbatim if the JSON decode fails. - var env errorEnvelope - if err := json.Unmarshal(body, &env); err == nil && env.Error != "" { - return nil, fmt.Errorf("GET %s: %d %s (%s)", parsed.Path, resp.StatusCode, env.Error, env.Message) - } - return nil, fmt.Errorf("GET %s: %d %s", parsed.Path, resp.StatusCode, strings.TrimSpace(string(body))) -} - // pickFocus chooses the most "interesting" resource from a fleet // sweep. The triage rule is lexicographic — severity dominates // count: any resource with a critical finding outranks every @@ -322,57 +245,19 @@ func focusLess(a, b fleetResource) bool { } // readOneSSEEvent opens the SSE stream, reads up to the first -// non-keepalive event payload, prints it, and returns. SSE is line- -// based with empty lines as record separators; this implementation -// is deliberately minimal — a few hundred bytes of stdlib code is -// enough to consume the substrate's push channel. -func readOneSSEEvent(ctx context.Context, fullURL, token string) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) - if err != nil { +// non-keepalive event payload through the shared Pulse Intelligence +// SSE parser, prints it, and returns. +func readOneSSEEvent(ctx context.Context, baseURL, path, token string) error { + seen := false + if err := agentcapabilities.StreamAgentActionableSSERecords(ctx, nil, baseURL, path, token, func(record agentcapabilities.SSERecord) bool { + fmt.Printf(" event: %s\n data: %s\n", record.Event, record.Data) + seen = true + return false + }); err != nil { return err } - req.Header.Set("X-API-Token", token) - req.Header.Set("Accept", "text/event-stream") - // No timeout on this client — context cancels the read. - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("subscribe: status %d", resp.StatusCode) - } - - scanner := bufio.NewScanner(resp.Body) - scanner.Buffer(make([]byte, 64*1024), 1<<20) - - var event, data string - skippedConnected := false - for scanner.Scan() { - line := scanner.Text() - switch { - case strings.HasPrefix(line, "event: "): - event = strings.TrimPrefix(line, "event: ") - case strings.HasPrefix(line, "data: "): - data = strings.TrimPrefix(line, "data: ") - case line == "": - // End of event record. The first event the server - // always emits is "stream.connected" — skip it once so - // the probe surfaces the first *real* event (or a - // heartbeat). - if event == "stream.connected" && !skippedConnected { - skippedConnected = true - event, data = "", "" - continue - } - if event != "" || data != "" { - fmt.Printf(" event: %s\n data: %s\n", event, data) - return nil - } - } - } - if err := scanner.Err(); err != nil { - return err + if seen { + return nil } return ctx.Err() } diff --git a/cmd/agent-probe/main_test.go b/cmd/agent-probe/main_test.go index 909081fe1..ad9941459 100644 --- a/cmd/agent-probe/main_test.go +++ b/cmd/agent-probe/main_test.go @@ -1,7 +1,15 @@ package main import ( + "context" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" "testing" + + "github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities" ) // TestPickFocus_PrefersCriticalThenWarningThenInfo pins the focus @@ -81,3 +89,95 @@ func TestPickFocus_PrefersCriticalThenWarningThenInfo(t *testing.T) { }) } } + +func TestFetchFleetUsesSharedCapabilityBodyExecutor(t *testing.T) { + var got struct { + Method string + Path string + Token string + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got.Method = r.Method + got.Path = r.URL.EscapedPath() + got.Token = r.Header.Get(agentcapabilities.AgentAPITokenHeader) + w.WriteHeader(http.StatusAccepted) + _, _ = w.Write([]byte(`{ + "generatedAt": "2026-06-18T07:00:00Z", + "resources": [ + { + "canonicalId": "vm:101", + "resourceType": "vm", + "resourceName": "web", + "findings": {"total": 1, "critical": 1, "warning": 0, "info": 0}, + "pendingApprovalCount": 2 + } + ] + }`)) + })) + defer server.Close() + + fleet, err := fetchFleet(context.Background(), server.Client(), server.URL, []agentcapabilities.Capability{{ + Name: agentcapabilities.FleetContextCapabilityName, + Method: http.MethodGet, + Path: "/api/agent/fleet-context", + }}, "probe-token") + if err != nil { + t.Fatalf("fetchFleet: %v", err) + } + if len(fleet.Resources) != 1 || fleet.Resources[0].CanonicalID != "vm:101" || fleet.Resources[0].Findings.Critical != 1 { + t.Fatalf("fleet = %+v", fleet) + } + if got.Method != http.MethodGet || got.Path != "/api/agent/fleet-context" { + t.Fatalf("request method/path = %s %s", got.Method, got.Path) + } + if got.Token != "probe-token" { + t.Fatalf("token = %q", got.Token) + } +} + +func TestReadOneSSEEventUsesSharedActionableFilter(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Accept") != agentcapabilities.AgentSSEAccept { + t.Errorf("Accept header = %q, want %q", r.Header.Get("Accept"), agentcapabilities.AgentSSEAccept) + } + if r.Header.Get(agentcapabilities.AgentAPITokenHeader) != "probe-token" { + t.Errorf("%s header = %q", agentcapabilities.AgentAPITokenHeader, r.Header.Get(agentcapabilities.AgentAPITokenHeader)) + } + w.Header().Set("Content-Type", agentcapabilities.AgentSSEAccept) + _, _ = w.Write([]byte(strings.Join([]string{ + "event: " + string(agentcapabilities.EventKindStreamConnected), + "data: {}", + "", + "event: " + string(agentcapabilities.EventKindHeartbeat), + "", + "event: " + string(agentcapabilities.EventKindFindingCreated), + "data: {\"findingId\":\"f1\"}", + "", + }, "\n"))) + })) + defer server.Close() + + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("pipe stdout: %v", err) + } + os.Stdout = w + err = readOneSSEEvent(context.Background(), server.URL, agentcapabilities.AgentEventsPath, "probe-token") + _ = w.Close() + os.Stdout = oldStdout + output, readErr := io.ReadAll(r) + if readErr != nil { + t.Fatalf("read stdout: %v", readErr) + } + if err != nil { + t.Fatalf("readOneSSEEvent: %v", err) + } + text := string(output) + if !strings.Contains(text, "event: "+string(agentcapabilities.EventKindFindingCreated)) { + t.Fatalf("stdout missing finding event: %s", text) + } + if strings.Contains(text, string(agentcapabilities.EventKindStreamConnected)) || strings.Contains(text, string(agentcapabilities.EventKindHeartbeat)) { + t.Fatalf("stdout included transport event: %s", text) + } +} diff --git a/cmd/pulse-mcp/README.md b/cmd/pulse-mcp/README.md index ec0153d38..1c963c63d 100644 --- a/cmd/pulse-mcp/README.md +++ b/cmd/pulse-mcp/README.md @@ -1,14 +1,40 @@ # pulse-mcp -`pulse-mcp` is a Model Context Protocol (MCP) server that wraps Pulse's -agent surface as a tool set. It lets Claude Desktop, Claude Code, and any -other MCP-speaking client drive Pulse natively: list findings, read the -fleet, drill into a resource, set operator intent, run governed actions. +`pulse-mcp` is the Model Context Protocol (MCP) adapter for Pulse +Intelligence. It exposes the same governed operations substrate that +Pulse Assistant uses inside the app, but as tools for Claude Desktop, +Claude Code, OpenCode, and any other MCP-speaking client: list findings, +read the fleet, drill into a resource, set operator intent, and run +governed actions. + +## Pulse Intelligence surface contract + + +- **Pulse Intelligence Core**: Canonical context, governed actions, safety gates, approval state, action audit, and verification shared by Pulse Assistant, Pulse MCP, and Pulse Patrol. +- **Pulse Patrol**: Patrol is the first-party operations surface: it watches, investigates, acts within the chosen Patrol mode, verifies outcomes, and records what happened. +- **Pulse Assistant** (the native Pulse surface): The contextual explanation, approval, and handoff surface for Patrol findings, governed actions, verification, and operator questions. Affordances: tools and interactive questions. +- **Pulse MCP**: The external-agent adapter that projects canonical Pulse Intelligence capabilities as MCP tools. Affordances: tools, resources, prompts, and capability metadata. + The adapter is manifest-driven. Every tool it exposes is one entry in -Pulse's hand-authored capabilities manifest at `/api/agent/capabilities`. -Adding a capability there extends this server automatically. There is no -hardcoded tool list to keep in sync. +Pulse's canonical capabilities manifest at `/api/agent/capabilities`. +Adding a request/response capability to the manifest-owned Pulse MCP surface +tool contract extends this server automatically. There is no hardcoded tool +list to keep in sync, and there is no separate MCP-only action path to maintain +beside Pulse Assistant. `tools/list` uses the shared Pulse Intelligence +projection to expose the manifest metadata in each tool description, including +route, auth scope, action mode (`read`, `mixed`, `write`), approval policy +(`scope_only`, `action_plan`), request/response shape, and stable error codes. +Operator-state writes plus governed action and finding lifecycle tools also +expose typed `inputSchema` definitions for their top-level arguments, including +stable enums for criticality, approval decisions, and dismissal reasons. +The same shared projection exposes `resources/list` and `resources/read` +when the manifest contains the canonical fleet and per-resource context +capabilities. Resource URIs use `pulse://resource/` and +`resources/read` returns the same JSON bundle as `get_resource_context`. +It also exposes `prompts/list` and `prompts/get` from the manifest-owned +`workflowPrompts` catalogue. Those prompts are workflow hints over the +manifest tools and resource URIs, not a separate MCP playbook layer. ## Install @@ -77,26 +103,42 @@ Use any of the install paths above. The rest of this guide assumes ### 2. Mint an API token -Pulse needs a token with `monitoring:read` for the read tools, and -`monitoring:write` if you want the write tools (`set_operator_state`, -`clear_operator_state`) to work. Mint one in **Settings → Security → API -Tokens**. +Pulse fetches the live capabilities manifest first, and every tool +description includes its required auth scope. For a read-only external +agent, start with `monitoring:read`. For the full published Pulse +Intelligence surface, mint a token with the current manifest scopes: + +`monitoring:read`, `monitoring:write`, `settings:read`, `settings:write`, and `ai:execute` + + +You can also mint narrower tokens for specific workflows. For example, +omit `settings:write` if the client should not add or edit monitored +infrastructure sources, and omit `ai:execute` if it should not review +Patrol findings or plan, approve, or execute governed actions. Mint the +token in **Settings → +Security → API Tokens**. ### 3. Wire it into your client -#### Claude Desktop + +Most MCP clients need the same manifest-owned runtime facts: server name `pulse`, command `pulse-mcp`, base URL flag `--base-url`, default URL `http://localhost:7655`, and token environment variable `PULSE_API_TOKEN`. +The generated examples below cover the currently declared config families: `OpenCode`, `Claude-style clients`, and `custom MCP clients`. +If your client uses a different outer config format, keep those runtime facts and adapt only the wrapper. -Edit `~/Library/Application Support/Claude/claude_desktop_config.json` -(macOS) or the platform equivalent. Add a `pulse` entry under -`mcpServers`: +#### OpenCode + +Uses OpenCode's top-level mcp object. +Add this to `opencode.json`, `opencode.jsonc`, and `~/.config/opencode/opencode.json`: ```json { - "mcpServers": { + "$schema": "https://opencode.ai/config.json", + "mcp": { "pulse": { - "command": "/usr/local/bin/pulse-mcp", - "args": ["--base-url", "http://localhost:7655"], - "env": { + "type": "local", + "command": ["pulse-mcp", "--base-url", "http://localhost:7655"], + "enabled": true, + "environment": { "PULSE_API_TOKEN": "your-token-here" } } @@ -104,11 +146,12 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` } ``` -Restart Claude Desktop. The Pulse tools appear in the tool picker. +Restart OpenCode or reload its config after saving. Pulse tools appear under the `pulse` MCP server name. -#### Claude Code +#### Claude-style clients -Add the server to your project's `.mcp.json` (or your user-level config): +Uses the common mcpServers object supported by Claude Desktop and Claude Code. +Use this shape for `Claude Desktop` and `Claude Code` in `~/Library/Application Support/Claude/claude_desktop_config.json` and `.mcp.json`: ```json { @@ -124,6 +167,14 @@ Add the server to your project's `.mcp.json` (or your user-level config): } ``` +Restart your client after saving the config. If the client cannot resolve `pulse-mcp` from `PATH`, use the full binary path. + +#### custom MCP clients + +Keeps the Pulse MCP command, base URL flag, and token environment variable while adapting the outer client config shape. +Use command `pulse-mcp`, pass `--base-url http://localhost:7655`, set `PULSE_API_TOKEN` to the API token, and keep the server name `pulse` when the client asks for one. + + ## Configuration | Flag | Default | Purpose | @@ -161,71 +212,88 @@ flag off and consume the SSE stream directly. When `--emit-notifications` is on, the `initialize` response advertises the supported event kinds under -`capabilities.experimental.pulseNotifications.kinds`. Clients that -don't understand the experimental block ignore it silently. +`capabilities.experimental.pulseNotifications.kinds`. That list is +projected from the shared Pulse Intelligence event vocabulary, so it +stays aligned with `/api/agent/events`. Clients that don't understand +the experimental block ignore it silently. ## What the tools do -The exact set is whatever your Pulse instance's manifest declares. As of -this writing, the published capabilities are: +The exact request/response tool set is whatever your Pulse instance's +manifest declares. The generated inventory below reflects the canonical +manifest in this checkout; run `tools/list` from your MCP client to see +the live set. + **Context (read-only):** -- `get_resource_context` returns the situated picture of one resource: - identity, operator-set state, active findings, pending approvals, - recent actions including refused dispatches with their stable error - tokens. -- `get_fleet_context` returns a thin per-resource rollup across the org - for triage: identity, operator flags, per-severity finding counts, - pending-approval count. +- `get_resource_context` (Get resource context, `GET /api/agent/resource-context/{resourceId}`, scope `monitoring:read`, mode `read`, approval `scope_only`): Return the situated picture of a resource — identity, operator-set state with maintenance-window-active flag, active findings, pending approvals scoped to this resource, recent actions including refused dispatches. Command fields are redacted for monitoring-read API tokens unless the token also has ai:execute. +- `get_fleet_context` (Get fleet context, `GET /api/agent/fleet-context`, scope `monitoring:read`, mode `read`, approval `scope_only`): Return a thin per-resource triage rollup across every resource visible to the org — identity, operator flags (intentionallyOffline, neverAutoRemediate, maintenanceWindowActive), per-severity finding counts (total/critical/warning/info), and pending-approval count. One read for 'where do I focus?'; follow up via get_resource_context for depth. +- `get_operations_loop_status` (Get Patrol work status, `GET /api/agent/operations-loop/status`, scope `monitoring:read`, mode `read`, approval `scope_only`): Return the current content-safe Patrol work status: Patrol issue evidence, pending approvals, governed decisions/actions, verified outcomes, Patrol control outcome evidence, compatibility aliases, and optional MCP readiness. The payload is count-only and deliberately omits finding ids, action ids, prompts, commands, resource names, and output. **Operator state (per-resource intent):** -- `get_operator_state` reads the operator-set state for a resource - (intentionally offline, never-auto-remediate, maintenance window, - criticality). -- `set_operator_state` replaces the entire record. The server populates - attribution (`setAt`, `setBy`) so client values cannot spoof it. -- `clear_operator_state` removes the entry. Idempotent. +- `get_operator_state` (Get operator state, `GET /api/resources/{resourceId}/operator-state`, scope `monitoring:read`, mode `read`, approval `scope_only`): Read the operator-set state for a resource (intentionally offline, never auto-remediate, maintenance window, criticality). +- `set_operator_state` (Set operator state, `PUT /api/resources/{resourceId}/operator-state`, scope `monitoring:write`, mode `write`, approval `scope_only`): Replace the operator-set state for a resource. URL canonicalId wins over body; server populates setAt and setBy from the authenticated identity. +- `clear_operator_state` (Clear operator state, `DELETE /api/resources/{resourceId}/operator-state`, scope `monitoring:write`, mode `write`, approval `scope_only`): Remove any operator-set state for a resource. Idempotent — succeeds whether or not an entry was present. **Findings (Patrol lifecycle):** -- `list_findings` returns every Patrol finding (active, dismissed, - resolved). Filter client-side. -- `acknowledge_finding` marks a finding as seen but keeps it visible. -- `snooze_finding` hides a finding for a duration in hours. -- `dismiss_finding` permanently dismisses a finding with a reason - (`not_an_issue`, `expected_behavior`, `will_fix_later`). -- `resolve_finding` manually marks a finding resolved. +- `list_findings` (List findings, `GET /api/ai/patrol/findings`, scope `ai:execute`, mode `read`, approval `scope_only`): List all Patrol findings (active, dismissed, resolved). Filter client-side on returned shape. +- `acknowledge_finding` (Acknowledge finding, `POST /api/ai/patrol/acknowledge`, scope `ai:execute`, mode `write`, approval `scope_only`): Mark a finding as seen but keep it visible. Auto-resolves when the underlying condition clears. +- `snooze_finding` (Snooze finding, `POST /api/ai/patrol/snooze`, scope `ai:execute`, mode `write`, approval `scope_only`): Hide a finding for a defined duration in hours. +- `dismiss_finding` (Dismiss finding, `POST /api/ai/patrol/dismiss`, scope `ai:execute`, mode `write`, approval `scope_only`): Dismiss a finding with a reason: not_an_issue (permanent suppression), expected_behavior (acknowledged forever), or will_fix_later (7-day reminder commitment). +- `resolve_finding` (Resolve finding, `POST /api/ai/patrol/resolve`, scope `ai:execute`, mode `write`, approval `scope_only`): Manually mark a finding as resolved when the underlying issue has been fixed out-of-band. **Actions (governed plan/approval/execute):** -- `plan_action` validates an action request, records the plan, and - returns approval policy, blast radius, plan hash, and preflight - summary. -- `decide_action` records an approval or rejection for a planned - action. -- `execute_action` executes a previously planned and approved action - and returns the persisted audit result. +- `plan_action` (Plan action, `POST /api/actions/plan`, scope `ai:execute`, mode `write`, approval `action_plan`): Plan an action against a resource. The planner validates the request, looks up the capability on the resource, checks executor-owned live availability, and returns an ActionPlan with the approval policy, blast radius, plan hash, and preflight summary. The plan is persisted to the audit history at the planned/pending state only after the live availability check passes, so subsequent decide_action and execute_action calls can reference it by id. Plan-and-execute is a two-step flow when the resulting plan requires approval, one-step otherwise. +- `decide_action` (Decide action, `POST /api/actions/{actionId}/decision`, scope `ai:execute`, mode `write`, approval `action_plan`): Record an approval decision (approved or rejected) on a previously planned action. The actor is taken from the authenticated identity; an explicit reason can be passed in the body. Idempotent on the persisted decision: re-deciding a non-pending action surfaces the action_not_pending stable code so agents can branch on the conflict rather than retrying blindly. +- `execute_action` (Execute action, `POST /api/actions/{actionId}/execute`, scope `ai:execute`, mode `write`, approval `action_plan`): Execute a previously planned and (when required) approved action. Returns the persisted audit record with the execution result attached. Refuses with stable codes when the action is in the wrong lifecycle state (action_not_approved, action_already_executing, action_execution_final, action_dry_run_only, action_plan_expired), when the approved plan no longer matches the current resource/capability contract (action_plan_drift), when the target is operator-locked against automated remediation (resource_remediation_locked), or when the API instance has no executor wired (action_executor_unavailable). action.completed SSE events fire on every terminal state so agents watching the stream do not need to poll this endpoint after dispatch. **Provisioning (infrastructure onboarding):** -- `discover_lan` scans a subnet, or returns cached scan results, to - find candidate infrastructure hosts before import. -- `list_nodes` returns configured Pulse infrastructure sources with - credential secret values redacted. -- `add_node` adds a Proxmox VE, Proxmox Backup Server, or Proxmox Mail - Gateway source. -- `update_node` updates a configured source without changing omitted - fields. -- `remove_node` removes a configured source. -- `test_node_credentials` validates proposed source credentials without - saving them. -- `test_node_connection` validates a saved source by id. -- `refresh_node_cluster_membership` re-detects Proxmox VE cluster - endpoint metadata for an existing source. +- `list_nodes` (List nodes, `GET /api/config/nodes`, scope `settings:read`, mode `read`, approval `scope_only`): List configured infrastructure sources that Pulse can monitor or manage. Credential secret values are redacted; use the returned id with update_node, remove_node, test_node_connection, or refresh_node_cluster_membership. +- `add_node` (Add node, `POST /api/config/nodes`, scope `settings:write`, mode `write`, approval `scope_only`): Add a Proxmox VE, Proxmox Backup Server, or Proxmox Mail Gateway source to Pulse after credentials have been collected, generated, or approved. +- `update_node` (Update node, `PUT /api/config/nodes/{nodeId}`, scope `settings:write`, mode `write`, approval `scope_only`): Update a configured infrastructure source. Omitted fields preserve the current value; tokenValue or password only changes when supplied. +- `remove_node` (Remove node, `DELETE /api/config/nodes/{nodeId}`, scope `settings:write`, mode `write`, approval `scope_only`): Remove a configured infrastructure source from Pulse by node id. +- `test_node_credentials` (Test node credentials, `POST /api/config/nodes/test-config`, scope `settings:write`, mode `mixed`, approval `scope_only`): Validate proposed source credentials and connection details without saving them to Pulse. +- `test_node_connection` (Test node connection, `POST /api/config/nodes/{nodeId}/test`, scope `settings:write`, mode `mixed`, approval `scope_only`): Validate the saved connection for an existing configured infrastructure source. +- `refresh_node_cluster_membership` (Refresh node cluster membership, `POST /api/config/nodes/{nodeId}/refresh-cluster`, scope `settings:write`, mode `write`, approval `scope_only`): Re-detect Proxmox VE cluster membership and endpoint metadata for a configured source. +- `discover_lan` (Discover LAN, `POST /api/discover`, scope `settings:write`, mode `mixed`, approval `scope_only`): Scan a subnet, or return cached scan results, to find candidate infrastructure hosts before deciding which sources to add to Pulse. + -Run `tools/list` from your MCP client to see the live set. +## Workflow prompts + +Clients that support MCP prompts can also discover Pulse workflow hints. +They are projected from the manifest-owned `workflowPrompts` catalogue: + + +- `pulse_triage_fleet` (Triage fleet): Triage the Pulse fleet using the canonical fleet context capability, then choose where deeper investigation is warranted. +- `pulse_operations_loop` (Ask Patrol to handle an issue): Have Patrol investigate active findings, follow the configured Patrol mode, take approved actions, verify the outcome, and record what happened. +- `pulse_investigate_resource` (Investigate resource; required argument `resourceId`): Investigate one Pulse resource using the canonical resource context capability and resource URI projection. +- `pulse_review_finding` (Review finding; required argument `finding_id`): Review one Patrol finding and propose the safest governed next step. + + +Run `prompts/list` from your MCP client to see the live prompt set. + +## Resource browser + +Clients that support MCP resources can call `resources/list` to browse +the same canonical resource IDs returned by `get_fleet_context`, then +`resources/read` to fetch the full `get_resource_context` JSON bundle +for one resource. + +Example URI: + +```text +pulse://resource/vm:101 +``` + +The adapter does not maintain a separate resource registry. The resource +list is rebuilt through the live fleet-context capability, and each read +is a normal per-resource context capability call with the same auth scope, +redaction rules, and error envelope as the tool surface. ## Stable error envelope @@ -239,13 +307,24 @@ The MCP tool result wraps that JSON in a text content block with `isError: true`. Branch on `error` (snake_case stable codes); use `message` for surfacing to humans, never for branching. -Capability-specific codes the substrate currently emits include -`resource_not_found` (depth read on an unknown id), -`operator_state_not_set` (read on a resource with no operator entry), and -`operator_state_invalid` (write rejected by the validator). Cross-cutting -codes from the auth / multi-tenant middleware (`invalid_org`, -`org_suspended`, `access_denied`) can apply to any authenticated tool -call. +Capability-specific stable codes are advertised by the manifest: + + +- `get_resource_context`: `resource_not_found` +- `get_operator_state`: `operator_state_not_set` +- `set_operator_state`: `operator_state_invalid` +- `acknowledge_finding`: `invalid_finding_request`, `finding_not_found`, `finding_action_not_allowed`, and `patrol_unavailable` +- `snooze_finding`: `invalid_finding_request`, `finding_not_found`, `finding_action_not_allowed`, and `patrol_unavailable` +- `dismiss_finding`: `invalid_finding_request`, `finding_not_found`, `finding_action_not_allowed`, and `patrol_unavailable` +- `resolve_finding`: `invalid_finding_request`, `finding_not_found`, `finding_action_not_allowed`, and `patrol_unavailable` +- `plan_action`: `invalid_action_request`, `resource_not_found`, `capability_not_found`, and `action_execution_unavailable` +- `decide_action`: `missing_id`, `invalid_id`, `invalid_action_decision`, `action_not_found`, `action_not_pending`, and `action_plan_expired` +- `execute_action`: `missing_id`, `invalid_id`, `invalid_action_execution`, `action_not_found`, `action_not_approved`, `action_already_executing`, `action_execution_final`, `action_dry_run_only`, `action_plan_expired`, `action_plan_drift`, `resource_remediation_locked`, and `action_executor_unavailable` + + +Cross-cutting codes from the auth / multi-tenant middleware +(`invalid_org`, `org_suspended`, `access_denied`) can apply to any +authenticated tool call. ## Known limitations @@ -263,10 +342,6 @@ call. lifetime. Restart `pulse-mcp` to pick up new capabilities after upgrading Pulse. -- **No resource URIs.** MCP supports a `resources/` channel in addition - to `tools/`. The adapter exposes only tools today; this is sufficient - for the substrate's current surface and keeps the adapter small. - ## Troubleshooting **"env var PULSE_API_TOKEN is empty" on startup.** @@ -281,9 +356,13 @@ the proxy is gating the manifest endpoint. Make sure `/api/agent/capabilities` is reachable without a credential, the same way `/api/health` is. -**Tools work, but `set_operator_state` returns 403 access_denied.** -Your token is missing `monitoring:write`. Either mint a new token with -both scopes, or restrict yourself to the read tools. +**Tools work, but a write or action tool returns 403 access_denied.** +Your token is missing that tool's required scope. Run `tools/list` and +check the `required scope` line in the tool description. Operator-state +writes need `monitoring:write`, provisioning tools need `settings:read` +or `settings:write`, and Patrol finding plus governed action tools need +`ai:execute`. Mint a narrower or broader token depending on which +external-agent workflows you want to allow. **Tools list is empty.** The adapter filters `subscribe_events` out (it is not request/response @@ -293,16 +372,20 @@ manifest is empty, which is a Pulse-side bug; check ## Implementation notes -The adapter is one stdlib-only Go file (`main.go`) in around 430 lines. -It speaks JSON-RPC 2.0 over stdio with line-delimited framing, logs to -stderr to keep the JSON-RPC channel on stdout clean, and derives each -tool's input schema from the manifest entry: `{name}` segments in the -declared path become required string properties, and non-GET/DELETE -tools accept a free-form `body` object. +The adapter binary is intentionally thin. It speaks JSON-RPC 2.0 over +stdio with line-delimited framing, logs to stderr to keep the JSON-RPC +channel on stdout clean, and delegates method semantics, tool projection, +call-argument normalization, HTTP projection, and result envelopes to the +shared Pulse Intelligence contracts in `internal/agentcapabilities`. +Tool input schemas come from the manifest-owned JSON Schema when present; +the shared projection only falls back to deriving path placeholders and a +permissive body object for capabilities that have not yet authored a +stricter schema. The companion worked example, `cmd/agent-probe`, walks the same substrate as a plain HTTP client and is a useful reference for anyone building a non-MCP integration. Together the two binaries demonstrate -the substrate's two consumer profiles: stdio MCP and HTTP API. Both are -manifest-driven and stdlib-only, so adding capabilities to Pulse extends -both without code changes here. +Pulse Intelligence's external-agent profiles: stdio MCP and direct HTTP +API. Pulse Assistant remains the first-party in-app surface over the +same governed contracts; `pulse-mcp` is the adapter for users who prefer +an external agent host. diff --git a/cmd/pulse-mcp/main.go b/cmd/pulse-mcp/main.go index 7e5d1097a..36bf0a1ac 100644 --- a/cmd/pulse-mcp/main.go +++ b/cmd/pulse-mcp/main.go @@ -1,11 +1,12 @@ -// Command pulse-mcp is a minimal MCP (Model Context Protocol) -// adapter that wraps Pulse's agent surface for stdio-speaking -// clients like Claude Desktop and Claude Code. It is the -// translation layer the substrate was designed to make cheap: -// every MCP tool here is a one-line projection of an entry in -// Pulse's hand-authored capabilities manifest. +// Command pulse-mcp is the Model Context Protocol adapter for +// Pulse Intelligence. It exposes the same governed operations +// substrate that Pulse Assistant uses natively, but for +// stdio-speaking external agents like OpenCode, Claude Desktop, +// Claude Code, and other MCP clients. It is deliberately a thin +// translation layer: every MCP tool here is a one-line projection +// of an entry in Pulse's canonical capabilities manifest. // -// Usage (typical Claude Desktop config entry): +// Usage (common mcpServers-style config entry): // // { // "mcpServers": { @@ -24,39 +25,46 @@ // // 1. Fetches /api/agent/capabilities from the configured Pulse // instance at startup. The manifest is the single source of -// truth — adding a capability there automatically extends the -// MCP tool surface here, no MCP-side changes required. +// truth — adding a request/response capability to the Pulse MCP +// surface tool contract automatically extends the MCP tool +// surface here, no MCP-side changes required. // // 2. Translates each capability into an MCP tool with: // - tool name = capability name (snake_case agent identifier) -// - description = capability description -// - inputSchema = derived from path placeholders + body shape: -// {resourceId} segments become required string properties; -// non-GET tools accept a free-form `body` object. +// - description = capability description plus manifest metadata +// (route, scope, action mode, approval policy, shapes, errors) +// - inputSchema = the manifest-owned JSON Schema when declared, +// with a path/body fallback for capabilities that have not yet +// authored a stricter schema. // -// 3. Handles the MCP JSON-RPC methods Claude actually calls: -// initialize, tools/list, tools/call. Each tools/call +// 3. Handles the MCP JSON-RPC methods agents actually call: +// initialize, tools/list, tools/call, resources/list, +// resources/read, prompts/list, and prompts/get. Each tools/call // resolves the manifest entry by name, substitutes path // params, makes the HTTP request to Pulse with the configured // token, and returns the JSON response (or stable error // envelope) as a text content block. // -// What it does not do (yet): +// What stays out of the request/response tool projection: // -// - subscribe_events. SSE streaming doesn't fit the MCP tool -// shape; it would be an MCP "notification" or a long-running -// tool. Future slices can layer this on; agents that need -// real-time push consume the SSE stream directly. +// - subscribe_events as a callable tool. SSE streaming doesn't +// fit the MCP request/response shape, so --emit-notifications +// bridges it into JSON-RPC notifications when an agent needs +// real-time push. // -// - Resource URIs. MCP supports `resources/list`/`resources/read` -// in addition to tools, but for Pulse the tool-only model is -// sufficient and keeps the adapter small. A future slice can -// project resource-context bundles as MCP resources if the -// UX value is clear. +// MCP resources are also manifest-backed: resources/list uses the +// canonical fleet context capability, and resources/read uses the +// canonical per-resource context capability. The resource URI shape +// and capability projection live in internal/agentcapabilities so +// this adapter does not grow a second inventory model. +// +// MCP prompts are manifest-backed workflow hints over that same +// tool/resource surface. The prompt catalog and prompt rendering live +// in internal/agentcapabilities so this adapter does not grow local +// operational playbooks. package main import ( - "bufio" "bytes" "context" "encoding/json" @@ -66,70 +74,26 @@ import ( "log" "net/http" "os" - "regexp" "strings" "sync" "time" + + "github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities" ) -// agentCapability mirrors Pulse's manifest wire shape — defined -// inline so the adapter depends on nothing in the pulse module. -type agentCapability struct { - Name string `json:"name"` - Description string `json:"description"` - Category string `json:"category"` - Method string `json:"method"` - Path string `json:"path"` - Scope string `json:"scope"` - ResponseShape string `json:"responseShape,omitempty"` - ErrorCodes []string `json:"errorCodes,omitempty"` - RequestBodyShape string `json:"requestBodyShape,omitempty"` - InputSchema json.RawMessage `json:"inputSchema,omitempty"` -} +type agentCapability = agentcapabilities.Capability +type agentCapabilitiesManifest = agentcapabilities.Manifest -type agentCapabilitiesManifest struct { - Version string `json:"version"` - Capabilities []agentCapability `json:"capabilities"` -} +type jsonRPCRequest = agentcapabilities.JSONRPCRequest -// jsonRPCRequest is the JSON-RPC 2.0 request envelope. Method is -// the MCP method name (e.g. "tools/list"); params is method- -// specific. ID is null for notifications, otherwise echoed back on -// the response. -type jsonRPCRequest struct { - JSONRPC string `json:"jsonrpc"` - ID json.RawMessage `json:"id,omitempty"` - Method string `json:"method"` - Params json.RawMessage `json:"params,omitempty"` -} - -type jsonRPCResponse struct { - JSONRPC string `json:"jsonrpc"` - ID json.RawMessage `json:"id,omitempty"` - Result any `json:"result,omitempty"` - Error *jsonRPCError `json:"error,omitempty"` -} - -type jsonRPCError struct { - Code int `json:"code"` - Message string `json:"message"` - Data any `json:"data,omitempty"` -} - -// MCP tool shape. inputSchema is JSON Schema (draft-07-ish) that -// the agent uses to validate before calling. -type mcpTool struct { - Name string `json:"name"` - Description string `json:"description"` - InputSchema json.RawMessage `json:"inputSchema"` -} - -// pathPlaceholderRE matches `{paramName}` in capability paths. -var pathPlaceholderRE = regexp.MustCompile(`\{([a-zA-Z][a-zA-Z0-9]*)\}`) +const ( + pulseMCPServerName = "pulse-mcp" + pulseMCPServerVersion = "0.1.0" +) func main() { - baseURL := flag.String("base-url", "http://localhost:7655", "Pulse base URL") - tokenEnv := flag.String("token-env", "PULSE_API_TOKEN", "Env var holding the Pulse API token") + baseURL := flag.String("base-url", agentcapabilities.DefaultMCPAdapterDefaultBaseURL, "Pulse base URL") + tokenEnv := flag.String("token-env", agentcapabilities.DefaultMCPAdapterTokenEnv, "Env var holding the Pulse API token") emitNotifications := flag.Bool("emit-notifications", false, "Translate Pulse SSE events into JSON-RPC notifications on stdout. Off by default because not every MCP client surfaces server-initiated notifications; enable when wiring an autonomous agent that processes the JSON-RPC stream.") flag.Parse() @@ -137,17 +101,18 @@ func main() { log.SetPrefix("pulse-mcp ") log.SetFlags(log.LstdFlags | log.Lshortfile) - token := strings.TrimSpace(os.Getenv(*tokenEnv)) - if token == "" { - log.Fatalf("env var %s is empty; pulse-mcp needs an API token with monitoring:read scope (and monitoring:write for set/clear operator-state)", *tokenEnv) - } - - manifest, err := fetchManifest(*baseURL) + manifestClient := &http.Client{Timeout: 10 * time.Second} + manifest, err := agentcapabilities.FetchManifest(context.Background(), manifestClient, *baseURL) if err != nil { log.Fatalf("could not fetch capabilities manifest from %s: %v", *baseURL, err) } log.Printf("fetched manifest %s with %d capabilities from %s", manifest.Version, len(manifest.Capabilities), *baseURL) + token := strings.TrimSpace(os.Getenv(*tokenEnv)) + if token == "" { + log.Fatal(missingAPITokenMessage(*tokenEnv, manifest)) + } + server := &mcpServer{ baseURL: *baseURL, token: token, @@ -158,6 +123,14 @@ func main() { server.serve(os.Stdin, os.Stdout) } +func missingAPITokenMessage(tokenEnv string, manifest *agentCapabilitiesManifest) string { + scopeList := agentcapabilities.ManifestRequiredScopeList(manifest) + if scopeList == "" { + return fmt.Sprintf("env var %s is empty; pulse-mcp needs an API token for the manifest tools you enable", tokenEnv) + } + return fmt.Sprintf("env var %s is empty; pulse-mcp needs an API token. Mint one with the manifest scopes for the tools you enable (current manifest scopes: %s)", tokenEnv, scopeList) +} + // mcpServer holds the per-process state: the configured Pulse base // URL and token, the manifest fetched at startup, and the HTTP // client used to call Pulse. @@ -165,7 +138,7 @@ type mcpServer struct { baseURL string token string manifest *agentCapabilitiesManifest - http *http.Client + http agentcapabilities.HTTPDoer mu sync.Mutex // guards stdout writes emitNotifications bool // notificationsOnce ensures the SSE consumer goroutine starts @@ -180,49 +153,46 @@ type mcpServer struct { out io.Writer } +type pulseMCPAgentSurfaceHTTPDoer struct { + next agentcapabilities.HTTPDoer +} + +func (d pulseMCPAgentSurfaceHTTPDoer) Do(req *http.Request) (*http.Response, error) { + if req != nil { + req = req.Clone(req.Context()) + req.Header = req.Header.Clone() + req.Header.Set(agentcapabilities.AgentSurfaceHeader, agentcapabilities.AgentSurfacePulseMCP) + } + next := d.next + if next == nil { + next = http.DefaultClient + } + return next.Do(req) +} + // serve is the stdio loop: read line-delimited JSON-RPC requests // from `in`, dispatch, write responses to `out`. Each request is on // its own line; blank lines are ignored; EOF stops the server. func (s *mcpServer) serve(in io.Reader, out io.Writer) { s.out = out - scanner := bufio.NewScanner(in) - scanner.Buffer(make([]byte, 64*1024), 1<<22) // up to 4 MB per message - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { - continue - } - var req jsonRPCRequest - if err := json.Unmarshal([]byte(line), &req); err != nil { - s.writeJSON(out, jsonRPCResponse{ - JSONRPC: "2.0", - Error: &jsonRPCError{ - Code: -32700, // Parse error - Message: fmt.Sprintf("malformed JSON-RPC request: %v", err), - }, - }) - continue - } - resp := s.dispatch(context.Background(), &req) - // Notifications (id is null/absent) get no response. - if len(req.ID) == 0 || string(req.ID) == "null" { - continue - } - s.writeJSON(out, resp) - } - if err := scanner.Err(); err != nil && err != io.EOF { - log.Printf("stdio scanner: %v", err) + if err := agentcapabilities.ServeJSONRPCLines( + context.Background(), + in, + func(ctx context.Context, req agentcapabilities.JSONRPCRequest) agentcapabilities.JSONRPCResponse { + return s.dispatch(ctx, &req) + }, + func(resp agentcapabilities.JSONRPCResponse) error { + return s.writeJSON(out, resp) + }, + ); err != nil { + log.Printf("stdio JSON-RPC: %v", err) } } -// dispatch routes one JSON-RPC request to the right handler. The -// MCP methods we support are minimal: initialize, tools/list, -// tools/call. Anything else gets method-not-found per JSON-RPC. -func (s *mcpServer) dispatch(ctx context.Context, req *jsonRPCRequest) jsonRPCResponse { - resp := jsonRPCResponse{JSONRPC: "2.0", ID: req.ID} - switch req.Method { - case "initialize": - resp.Result = s.handleInitialize() +// dispatch delegates MCP method semantics to the shared Pulse Intelligence +// boundary. This binary owns session policy and stdout serialization only. +func (s *mcpServer) dispatch(ctx context.Context, req *jsonRPCRequest) agentcapabilities.JSONRPCResponse { + return agentcapabilities.DispatchMCPToolServerRequest(ctx, *req, s.manifestToolServer().Handlers(func() { // Start the SSE-to-notifications bridge once per process, // only if the operator opted in. Spawned after the // initialize response is queued so the client sees the @@ -232,265 +202,79 @@ func (s *mcpServer) dispatch(ctx context.Context, req *jsonRPCRequest) jsonRPCRe go s.consumeSSEEvents(context.Background()) }) } - case "tools/list": - resp.Result = s.handleToolsList() - case "tools/call": - result, err := s.handleToolsCall(ctx, req.Params) - if err != nil { - resp.Error = &jsonRPCError{Code: -32603, Message: err.Error()} - } else { - resp.Result = result - } - case "ping": - resp.Result = map[string]any{} - default: - resp.Error = &jsonRPCError{ - Code: -32601, - Message: fmt.Sprintf("method not found: %s", req.Method), - } - } - return resp + })) } -// handleInitialize returns the MCP server's capabilities. Tools -// are the only request/response category we expose. When the -// operator opts in via --emit-notifications, the server also -// publishes server-initiated notifications translated from -// Pulse's SSE event stream; resources, prompts, and sampling -// remain intentionally unadvertised. -func (s *mcpServer) handleInitialize() any { - caps := map[string]any{ - "tools": map[string]any{}, +func (s *mcpServer) manifestToolServer() agentcapabilities.MCPManifestToolServer { + var manifest agentcapabilities.Manifest + if s.manifest != nil { + manifest = *s.manifest } - // MCP advertises notifications via experimental/extension - // keys today. We use a clearly-namespaced "experimental" - // block so clients that don't understand it ignore it - // silently rather than erroring on unknown keys. - if s.emitNotifications { - caps["experimental"] = map[string]any{ - "pulseNotifications": map[string]any{ - "kinds": []string{ - "finding.created", - "approval.pending", - "action.completed", - }, - }, - } - } - return map[string]any{ - "protocolVersion": "2024-11-05", - "capabilities": caps, - "serverInfo": map[string]any{ - "name": "pulse-mcp", - "version": "0.1.0", - }, + return agentcapabilities.MCPManifestToolServer{ + ServerName: pulseMCPServerName, + ServerVersion: pulseMCPServerVersion, + SurfaceID: agentcapabilities.SurfaceIDPulseMCP, + EmitPulseNotifications: s.emitNotifications, + Client: s.pulseMCPHTTPClient(), + BaseURL: s.baseURL, + Token: s.token, + Manifest: manifest, + RecordWorkflowPromptActivity: s.recordWorkflowPromptActivity, } } -// handleToolsList projects each manifest capability into an MCP -// tool. subscribe_events is filtered out — SSE streaming doesn't -// fit the request/response tool shape. -func (s *mcpServer) handleToolsList() any { - tools := make([]mcpTool, 0, len(s.manifest.Capabilities)) - for _, cap := range s.manifest.Capabilities { - if cap.Name == "subscribe_events" { - continue - } - schema := buildInputSchema(cap) - tools = append(tools, mcpTool{ - Name: cap.Name, - Description: cap.Description, - InputSchema: schema, - }) +func (s *mcpServer) pulseMCPHTTPClient() agentcapabilities.HTTPDoer { + if s == nil { + return pulseMCPAgentSurfaceHTTPDoer{} } - return map[string]any{"tools": tools} + return pulseMCPAgentSurfaceHTTPDoer{next: s.http} } -// handleToolsCall executes one tool invocation. params is shaped -// `{"name": "...", "arguments": {...}}`. The tool name resolves -// to a manifest capability; arguments fill path placeholders and -// (for non-GET tools) the request body. -func (s *mcpServer) handleToolsCall(ctx context.Context, raw json.RawMessage) (any, error) { - var params struct { - Name string `json:"name"` - Arguments map[string]any `json:"arguments"` - } - if err := json.Unmarshal(raw, ¶ms); err != nil { - return nil, fmt.Errorf("decode tools/call params: %w", err) - } - cap, ok := s.findCapability(params.Name) - if !ok { - return nil, fmt.Errorf("unknown tool: %s", params.Name) +func (s *mcpServer) recordWorkflowPromptActivity(ctx context.Context, promptName string) { + promptName = strings.TrimSpace(promptName) + if s == nil || promptName == "" || strings.TrimSpace(s.baseURL) == "" || strings.TrimSpace(s.token) == "" { + return } + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() - url, err := substitutePathParams(cap.Path, params.Arguments) + body, err := json.Marshal(map[string]string{"name": promptName}) if err != nil { - return nil, fmt.Errorf("substitute path params: %w", err) + log.Printf("workflow prompt activity: marshal: %v", err) + return } - - var body io.Reader - if cap.Method != http.MethodGet && cap.Method != http.MethodDelete { - // Non-GET/DELETE tools accept a `body` argument that's - // JSON-encoded as the request body. If absent, we send an - // empty object — that's fine for the finding-action - // capabilities that just need `{ "finding_id": "..." }`. - bodyArg, ok := params.Arguments["body"] - if !ok { - // Some capabilities take their body fields at the top - // level (no nested "body" key). Treat the whole - // arguments object as the body, minus any consumed - // path-placeholder keys. - pathParams := pathParamSet(cap.Path) - cleaned := map[string]any{} - for k, v := range params.Arguments { - if !pathParams[k] { - cleaned[k] = v - } - } - bodyArg = cleaned - } - buf, err := json.Marshal(bodyArg) - if err != nil { - return nil, fmt.Errorf("marshal request body: %w", err) - } - body = bytes.NewReader(buf) - } - - httpReq, err := http.NewRequestWithContext(ctx, cap.Method, s.baseURL+url, body) + req, err := agentcapabilities.NewAgentHTTPRequest( + ctx, + http.MethodPost, + s.baseURL, + agentcapabilities.AgentWorkflowPromptActivityPath, + s.token, + bytes.NewReader(body), + ) if err != nil { - return nil, fmt.Errorf("build request: %w", err) + log.Printf("workflow prompt activity: %v", err) + return } - httpReq.Header.Set("X-API-Token", s.token) - if body != nil { - httpReq.Header.Set("Content-Type", "application/json") - } - - resp, err := s.http.Do(httpReq) + resp, err := s.pulseMCPHTTPClient().Do(req) if err != nil { - return nil, fmt.Errorf("call Pulse: %w", err) + log.Printf("workflow prompt activity: %v", err) + return } defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("read response: %w", err) + _, _ = io.Copy(io.Discard, resp.Body) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + log.Printf("workflow prompt activity: status %d", resp.StatusCode) } - - // Build the MCP content result. The substrate's stable error - // envelope ({"error": "code", "message": "..."}) is preserved - // verbatim — agents on the MCP side branch on the same code - // they would branching on the HTTP response. - text := string(respBody) - isError := resp.StatusCode < 200 || resp.StatusCode >= 300 - return map[string]any{ - "content": []map[string]any{ - { - "type": "text", - "text": text, - }, - }, - "isError": isError, - }, nil } -func (s *mcpServer) findCapability(name string) (agentCapability, bool) { - for _, c := range s.manifest.Capabilities { - if c.Name == name { - return c, true - } - } - return agentCapability{}, false -} - -func (s *mcpServer) writeJSON(out io.Writer, v any) { +func (s *mcpServer) writeJSON(out io.Writer, v any) error { s.mu.Lock() defer s.mu.Unlock() - enc := json.NewEncoder(out) - enc.SetEscapeHTML(false) - if err := enc.Encode(v); err != nil { + if err := agentcapabilities.WriteJSONRPCMessage(out, v); err != nil { log.Printf("encode response: %v", err) + return err } -} - -// buildInputSchema generates a permissive JSON Schema for a -// capability. Path placeholders become required string properties; -// non-GET/DELETE capabilities also accept a free-form `body` -// object. The schema is permissive on purpose — the manifest is -// the source of truth for what the underlying endpoint accepts; -// MCP just needs enough shape so the agent knows which fields to -// pass. -func buildInputSchema(cap agentCapability) json.RawMessage { - if len(cap.InputSchema) > 0 { - return cap.InputSchema - } - - properties := map[string]any{} - required := []string{} - for _, m := range pathPlaceholderRE.FindAllStringSubmatch(cap.Path, -1) { - name := m[1] - properties[name] = map[string]any{ - "type": "string", - "description": "Canonical " + name + " (e.g. \"vm:101\", \"container:web-1\")", - } - required = append(required, name) - } - if cap.Method != http.MethodGet && cap.Method != http.MethodDelete { - desc := "Request body fields" - if cap.RequestBodyShape != "" { - desc = "Request body fields. Shape hint: " + cap.RequestBodyShape - } - properties["body"] = map[string]any{ - "type": "object", - "description": desc, - "additionalProperties": true, - } - } - schema := map[string]any{ - "type": "object", - "properties": properties, - "additionalProperties": true, - } - if len(required) > 0 { - schema["required"] = required - } - out, _ := json.Marshal(schema) - return out -} - -// substitutePathParams replaces `{name}` segments in a capability's -// path with the corresponding argument value. Missing args for -// declared placeholders are an error so the agent gets a stable -// failure rather than an HTTP 404 on a malformed URL. -func substitutePathParams(path string, args map[string]any) (string, error) { - var missing []string - out := pathPlaceholderRE.ReplaceAllStringFunc(path, func(match string) string { - name := match[1 : len(match)-1] // strip { and } - v, ok := args[name] - if !ok { - missing = append(missing, name) - return match - } - s, ok := v.(string) - if !ok { - missing = append(missing, name+" (not a string)") - return match - } - return s - }) - if len(missing) > 0 { - return "", fmt.Errorf("missing path argument(s): %s", strings.Join(missing, ", ")) - } - return out, nil -} - -// pathParamSet returns the set of placeholder names declared in a -// path. Used to filter path args out of the request body when a -// caller passes everything at the top level. -func pathParamSet(path string) map[string]bool { - set := map[string]bool{} - for _, m := range pathPlaceholderRE.FindAllStringSubmatch(path, -1) { - set[m[1]] = true - } - return set + return nil } // consumeSSEEvents opens a long-lived SSE connection to Pulse's @@ -507,10 +291,9 @@ func pathParamSet(path string) map[string]bool { // stays up across the kinds of stream stalls the MCP server's // idle-tolerance budget already accepts on the substrate side. func (s *mcpServer) consumeSSEEvents(ctx context.Context) { - url := s.baseURL + "/api/agent/events" backoff := time.Second for ctx.Err() == nil { - if err := s.streamSSEOnce(ctx, url); err != nil { + if err := s.streamSSEOnce(ctx, agentcapabilities.AgentEventsPath); err != nil { if ctx.Err() != nil { return } @@ -532,95 +315,13 @@ func (s *mcpServer) consumeSSEEvents(ctx context.Context) { } // streamSSEOnce opens one SSE connection and reads events until -// the connection drops or the context is cancelled. Each parsed -// event becomes a JSON-RPC notification; the initial -// stream.connected event is skipped (it's a synchronization marker -// from the server, not an agent-actionable event). -func (s *mcpServer) streamSSEOnce(ctx context.Context, url string) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return err - } - req.Header.Set("X-API-Token", s.token) - req.Header.Set("Accept", "text/event-stream") - // Bypass the s.http client's Timeout — the SSE stream is - // long-lived; ctx is the right cancellation signal. - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("subscribe: status %d", resp.StatusCode) - } - - scanner := bufio.NewScanner(resp.Body) - scanner.Buffer(make([]byte, 64*1024), 1<<22) - - var event, data string - for scanner.Scan() { - line := scanner.Text() - switch { - case strings.HasPrefix(line, "event: "): - event = strings.TrimPrefix(line, "event: ") - case strings.HasPrefix(line, "data: "): - data = strings.TrimPrefix(line, "data: ") - case line == "": - s.maybeEmitNotification(event, data) - event, data = "", "" - } - } - if err := scanner.Err(); err != nil { - return err - } - return nil -} - -// maybeEmitNotification turns a parsed SSE record into a JSON-RPC -// notification, filtering out keepalives and the connect marker -// that aren't agent-actionable. The notification's params is the -// raw JSON of the SSE data field, so the receiver gets the same -// payload the substrate publishes. -func (s *mcpServer) maybeEmitNotification(event, data string) { - if event == "" || data == "" { - return - } - // Skip events that are pure transport plumbing — agents - // branching on event kind don't care about these. - if event == "stream.connected" || event == "heartbeat" { - return - } - var params json.RawMessage = []byte(data) - notification := jsonRPCRequest{ - JSONRPC: "2.0", - Method: "notifications/" + event, - Params: params, - } - s.writeJSON(s.out, notification) -} - -// fetchManifest pulls the capabilities manifest from Pulse. This -// is the only call the adapter makes before its first tool -// invocation; the manifest is not cached or refreshed during the -// process lifetime — restart pulse-mcp to pick up new -// capabilities. -func fetchManifest(baseURL string) (*agentCapabilitiesManifest, error) { - req, err := http.NewRequest(http.MethodGet, baseURL+"/api/agent/capabilities", nil) - if err != nil { - return nil, err - } - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("manifest GET returned %d", resp.StatusCode) - } - var m agentCapabilitiesManifest - if err := json.NewDecoder(resp.Body).Decode(&m); err != nil { - return nil, fmt.Errorf("decode manifest: %w", err) - } - return &m, nil +// the connection drops or the context is cancelled. The shared +// Pulse Intelligence core owns SSE-to-MCP notification projection; +// the adapter only serializes projected notifications to stdout. +func (s *mcpServer) streamSSEOnce(ctx context.Context, path string) error { + // Bypass the finite s.http timeout: the SSE stream is long-lived and ctx is + // the right cancellation signal. The wrapper still marks the adapter surface. + return agentcapabilities.StreamMCPEventNotifications(ctx, pulseMCPAgentSurfaceHTTPDoer{}, s.baseURL, path, s.token, func(notification agentcapabilities.JSONRPCRequest) error { + return s.writeJSON(s.out, notification) + }) } diff --git a/cmd/pulse-mcp/main_test.go b/cmd/pulse-mcp/main_test.go index ca86bb560..c874568a2 100644 --- a/cmd/pulse-mcp/main_test.go +++ b/cmd/pulse-mcp/main_test.go @@ -7,24 +7,182 @@ import ( "io" "net/http" "net/http/httptest" + "os" "strings" "sync" "testing" + + "github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities" ) -// TestBuildInputSchema_PathPlaceholdersBecomeRequiredStringProps +type httpDoerFunc func(*http.Request) (*http.Response, error) + +func (f httpDoerFunc) Do(req *http.Request) (*http.Response, error) { + return f(req) +} + +func TestAgentCapabilityUsesSharedWireContract(t *testing.T) { + rawSchema := json.RawMessage(`{"type":"object","additionalProperties":false}`) + rawOutputSchema := json.RawMessage(`{"type":"object","properties":{"resources":{"type":"array"}}}`) + capability := agentCapability{ + Name: "get_fleet_context", + Method: http.MethodGet, + Path: "/api/agent/fleet-context", + ActionMode: agentcapabilities.ActionModeRead, + InputSchema: rawSchema, + OutputSchema: rawOutputSchema, + } + manifest := agentCapabilitiesManifest{ + Version: "v1", + Capabilities: []agentcapabilities.Capability{capability}, + } + var shared agentCapability = manifest.Capabilities[0] + + if shared.Name != capability.Name { + t.Fatalf("shared manifest capability name = %q, want %q", shared.Name, capability.Name) + } + if shared.ActionMode != agentcapabilities.ActionModeRead { + t.Fatalf("shared manifest action mode = %q, want %q", shared.ActionMode, agentcapabilities.ActionModeRead) + } + if string(shared.InputSchema) != string(rawSchema) { + t.Fatalf("shared manifest input schema = %s, want %s", shared.InputSchema, rawSchema) + } + if string(shared.OutputSchema) != string(rawOutputSchema) { + t.Fatalf("shared manifest output schema = %s, want %s", shared.OutputSchema, rawOutputSchema) + } +} + +func TestMissingAPITokenMessageUsesManifestScopeSummary(t *testing.T) { + msg := missingAPITokenMessage("PULSE_API_TOKEN", &agentCapabilitiesManifest{ + RequiredScopes: []string{ + "settings:write", + "monitoring:read", + "ai:execute", + "monitoring:read", + }, + Capabilities: []agentCapability{ + {Name: "legacy_extra", Scope: "monitoring:write"}, + }, + }) + + for _, want := range []string{ + "env var PULSE_API_TOKEN is empty", + "current manifest scopes: monitoring:read, settings:write, ai:execute", + } { + if !strings.Contains(msg, want) { + t.Fatalf("missing token message missing %q in %q", want, msg) + } + } + if strings.Count(msg, "monitoring:read") != 1 { + t.Fatalf("missing token message must deduplicate scopes; got %q", msg) + } +} + +func TestMissingAPITokenMessageFallsBackForLegacyManifest(t *testing.T) { + msg := missingAPITokenMessage("PULSE_API_TOKEN", &agentCapabilitiesManifest{ + Capabilities: []agentCapability{ + {Name: "write_settings", Scope: "settings:write"}, + {Name: "read_monitoring", Scope: "monitoring:read"}, + }, + }) + + if !strings.Contains(msg, "current manifest scopes: monitoring:read, settings:write") { + t.Fatalf("missing token message should fall back to capability scopes for legacy manifests; got %q", msg) + } +} + +func TestMainTokenGuidanceUsesSharedManifestScopeSummary(t *testing.T) { + source, err := os.ReadFile("main.go") + if err != nil { + t.Fatalf("read main.go: %v", err) + } + text := string(source) + if !strings.Contains(text, "agentcapabilities.ManifestRequiredScopeList(manifest)") { + t.Fatal("pulse-mcp token guidance must consume the manifest-owned requiredScopes summary") + } + if strings.Contains(text, "RequiredCapabilityScopeList(capabilities)") { + t.Fatal("pulse-mcp token guidance must not recompute current manifest scopes from capability rows") + } + if strings.Contains(text, "monitoring:read scope (and monitoring:write") { + t.Fatal("pulse-mcp must not hardcode partial token-scope guidance in startup errors") + } +} + +func TestMainUsesSharedMCPAdapterRuntimeDefaults(t *testing.T) { + source, err := os.ReadFile("main.go") + if err != nil { + t.Fatalf("read main.go: %v", err) + } + text := string(source) + for _, want := range []string{ + `flag.String("base-url", agentcapabilities.DefaultMCPAdapterDefaultBaseURL`, + `flag.String("token-env", agentcapabilities.DefaultMCPAdapterTokenEnv`, + } { + if !strings.Contains(text, want) { + t.Fatalf("pulse-mcp runtime defaults must come from the shared MCP adapter contract; missing %s", want) + } + } +} + +func TestReadmePresentsSharedMCPClientConfig(t *testing.T) { + source, err := os.ReadFile("README.md") + if err != nil { + t.Fatalf("read README.md: %v", err) + } + text := string(source) + for _, want := range []string{ + agentcapabilities.MCPReadmeClientConfigStartMarker, + agentcapabilities.MCPReadmeClientConfigEndMarker, + "Most MCP clients need the same manifest-owned runtime facts", + "server name `pulse`, command `pulse-mcp`, base URL flag `--base-url`", + "currently declared config families: `OpenCode`, `Claude-style clients`, and `custom MCP clients`", + "Uses OpenCode's top-level mcp object.", + `"type": "local"`, + `"command": ["pulse-mcp", "--base-url", "http://localhost:7655"]`, + `"environment": {`, + "Uses the common mcpServers object supported by Claude Desktop and Claude Code.", + "Use command `pulse-mcp`, pass `--base-url http://localhost:7655`", + "Restart your client after saving the config.", + "Claude Desktop", + "Claude Code", + "OpenCode", + } { + if !strings.Contains(text, want) { + t.Fatalf("README.md must include shared MCP client setup copy %q", want) + } + } + if strings.Contains(text, "Restart Claude Desktop. The Pulse tools appear") { + t.Fatal("README.md must not present Pulse MCP setup as Claude Desktop-only") + } + if strings.Contains(text, "The examples below use the common\n`mcpServers` shape") { + t.Fatal("README.md must not present OpenCode as a generic mcpServers wrapper") + } +} + +func testPulseMCPSurfaceToolContracts(toolNames ...string) []agentcapabilities.SurfaceToolContract { + return []agentcapabilities.SurfaceToolContract{ + { + SurfaceID: agentcapabilities.SurfaceIDPulseMCP, + ToolSource: agentcapabilities.SurfaceToolSourceCapabilityManifest, + ToolNames: append([]string(nil), toolNames...), + Affordances: agentcapabilities.DefaultSurfaceAffordancesForID(agentcapabilities.SurfaceIDPulseMCP), + }, + } +} + +// TestToolInputSchema_PathPlaceholdersBecomeRequiredStringProps // pins the rule that turns capability paths into MCP tool input // schemas: every {name} segment in the path becomes a required // string property the agent must supply, with a description that // hints at the canonical shape ("vm:101", "container:web-1") so // the LLM forms the right id without back-and-forth. -func TestBuildInputSchema_PathPlaceholdersBecomeRequiredStringProps(t *testing.T) { +func TestToolInputSchema_PathPlaceholdersBecomeRequiredStringProps(t *testing.T) { cap := agentCapability{ Name: "get_resource_context", Path: "/api/agent/resource-context/{resourceId}", Method: http.MethodGet, } - raw := buildInputSchema(cap) + raw := agentcapabilities.ToolInputSchema(cap) var schema map[string]any if err := json.Unmarshal(raw, &schema); err != nil { t.Fatalf("unmarshal schema: %v", err) @@ -48,13 +206,13 @@ func TestBuildInputSchema_PathPlaceholdersBecomeRequiredStringProps(t *testing.T } } -// TestBuildInputSchema_NonGetCapabilitiesAcceptBody pins that +// TestToolInputSchema_NonGetCapabilitiesAcceptBody pins that // non-GET/DELETE tools expose a `body` property the agent fills // with the request payload. GET tools must NOT advertise a body // property so the agent doesn't try to send one (which would be // dropped by net/http anyway, but advertising it would be // misleading). -func TestBuildInputSchema_NonGetCapabilitiesAcceptBody(t *testing.T) { +func TestToolInputSchema_NonGetCapabilitiesAcceptBody(t *testing.T) { get := agentCapability{Path: "/api/foo", Method: http.MethodGet} post := agentCapability{ Path: "/api/foo", @@ -74,7 +232,7 @@ func TestBuildInputSchema_NonGetCapabilitiesAcceptBody(t *testing.T) { "DELETE": {del, false}, } { t.Run(name, func(t *testing.T) { - raw := buildInputSchema(tc.cap) + raw := agentcapabilities.ToolInputSchema(tc.cap) var schema map[string]any if err := json.Unmarshal(raw, &schema); err != nil { t.Fatalf("unmarshal schema: %v", err) @@ -88,7 +246,28 @@ func TestBuildInputSchema_NonGetCapabilitiesAcceptBody(t *testing.T) { } } -func TestBuildInputSchema_UsesManifestProvidedSchema(t *testing.T) { +func TestToolInputSchema_FallbackUsesSharedPermissiveEnvelope(t *testing.T) { + raw := agentcapabilities.ToolInputSchema(agentCapability{ + Path: "/api/resources/{id}/operator-state", + Method: http.MethodPut, + }) + var schema map[string]any + if err := json.Unmarshal(raw, &schema); err != nil { + t.Fatalf("unmarshal schema: %v", err) + } + if schema["additionalProperties"] != true { + t.Fatalf("fallback schema additionalProperties = %v, want true", schema["additionalProperties"]) + } + props, _ := schema["properties"].(map[string]any) + if _, hasID := props["id"]; !hasID { + t.Fatalf("fallback schema must include path argument id: %v", props) + } + if _, hasBody := props["body"]; !hasBody { + t.Fatalf("fallback schema must include body property for non-GET methods: %v", props) + } +} + +func TestToolInputSchema_UsesManifestProvidedSchema(t *testing.T) { rawSchema := json.RawMessage(`{ "type": "object", "properties": { @@ -105,7 +284,7 @@ func TestBuildInputSchema_UsesManifestProvidedSchema(t *testing.T) { InputSchema: rawSchema, } - got := buildInputSchema(cap) + got := agentcapabilities.ToolInputSchema(cap) var schema map[string]any if err := json.Unmarshal(got, &schema); err != nil { t.Fatalf("unmarshal schema: %v", err) @@ -118,25 +297,99 @@ func TestBuildInputSchema_UsesManifestProvidedSchema(t *testing.T) { } } -func TestSubstitutePathParams_FillsAllPlaceholders(t *testing.T) { - got, err := substitutePathParams( +func TestToolInputSchema_PreservesManifestActionArgumentSchema(t *testing.T) { + rawSchema := json.RawMessage(`{ + "type": "object", + "properties": { + "actionId": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" }, + "outcome": { "type": "string", "enum": ["approved", "rejected"] }, + "reason": { "type": "string" } + }, + "required": ["actionId", "outcome"], + "additionalProperties": false + }`) + cap := agentCapability{ + Name: "decide_action", + Path: "/api/actions/{actionId}/decision", + Method: http.MethodPost, + InputSchema: rawSchema, + } + + got := agentcapabilities.ToolInputSchema(cap) + var schema map[string]any + if err := json.Unmarshal(got, &schema); err != nil { + t.Fatalf("unmarshal schema: %v", err) + } + props, _ := schema["properties"].(map[string]any) + if _, hasActionID := props["actionId"]; !hasActionID { + t.Fatalf("manifest-provided action schema must preserve path argument actionId: %s", string(got)) + } + if _, hasBody := props["body"]; hasBody { + t.Fatalf("manifest-provided action schema must not be replaced by generic body wrapper: %s", string(got)) + } + + operatorStateSchema := json.RawMessage(`{ + "type": "object", + "properties": { + "resourceId": { "type": "string" }, + "intentionallyOffline": { "type": "boolean" }, + "neverAutoRemediate": { "type": "boolean" } + }, + "required": ["resourceId", "intentionallyOffline", "neverAutoRemediate"], + "additionalProperties": false + }`) + operatorCap := agentCapability{ + Name: agentcapabilities.SetOperatorStateCapabilityName, + Path: agentcapabilities.OperatorStateCapabilityPath, + Method: http.MethodPut, + InputSchema: operatorStateSchema, + } + + got = agentcapabilities.ToolInputSchema(operatorCap) + if err := json.Unmarshal(got, &schema); err != nil { + t.Fatalf("unmarshal operator-state schema: %v", err) + } + props, _ = schema["properties"].(map[string]any) + if _, hasResourceID := props["resourceId"]; !hasResourceID { + t.Fatalf("manifest-provided operator-state schema must preserve resourceId path argument: %s", string(got)) + } + if _, hasBody := props["body"]; hasBody { + t.Fatalf("manifest-provided operator-state schema must not be replaced by generic body wrapper: %s", string(got)) + } +} + +func TestSubstitutePathParameters_FillsAllPlaceholders(t *testing.T) { + got, err := agentcapabilities.SubstitutePathParameters( "/api/agent/resource-context/{resourceId}", map[string]any{"resourceId": "vm:101"}, ) if err != nil { t.Fatalf("unexpected error: %v", err) } - if got != "/api/agent/resource-context/vm:101" { - t.Errorf("got %q, want /api/agent/resource-context/vm:101", got) + if got != "/api/agent/resource-context/vm%3A101" { + t.Errorf("got %q, want /api/agent/resource-context/vm%%3A101", got) } } -func TestSubstitutePathParams_MissingPlaceholderIsAStableError(t *testing.T) { +func TestSubstitutePathParameters_EscapesReservedCharacters(t *testing.T) { + got, err := agentcapabilities.SubstitutePathParameters( + "/api/config/nodes/{nodeId}/test", + map[string]any{"nodeId": "pve/lab node"}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "/api/config/nodes/pve%2Flab%20node/test" { + t.Errorf("got %q, want escaped node id in a single path segment", got) + } +} + +func TestSubstitutePathParameters_MissingPlaceholderIsAStableError(t *testing.T) { // The agent must get a clear error when it forgets a path // argument — better to fail with "missing path argument // resourceId" than to send a literal `{resourceId}` URL to // Pulse and get a confusing 404. - _, err := substitutePathParams( + _, err := agentcapabilities.SubstitutePathParameters( "/api/agent/resource-context/{resourceId}", map[string]any{}, ) @@ -148,8 +401,8 @@ func TestSubstitutePathParams_MissingPlaceholderIsAStableError(t *testing.T) { } } -func TestSubstitutePathParams_NonStringIsAnError(t *testing.T) { - _, err := substitutePathParams( +func TestSubstitutePathParameters_NonStringIsAnError(t *testing.T) { + _, err := agentcapabilities.SubstitutePathParameters( "/api/resources/{id}/operator-state", map[string]any{"id": 12345}, ) @@ -164,7 +417,8 @@ func TestSubstitutePathParams_NonStringIsAnError(t *testing.T) { // `tools` is present. The server must advertise tools so Claude // (Desktop / Code) bothers to enumerate the surface. func TestServer_DispatchInitializeReturnsToolsCapability(t *testing.T) { - s := &mcpServer{manifest: &agentCapabilitiesManifest{Version: "v1"}} + manifest := agentcapabilities.CanonicalManifest() + s := &mcpServer{manifest: &manifest} resp := s.dispatch(context.Background(), &jsonRPCRequest{ JSONRPC: "2.0", ID: json.RawMessage(`1`), @@ -173,31 +427,355 @@ func TestServer_DispatchInitializeReturnsToolsCapability(t *testing.T) { if resp.Error != nil { t.Fatalf("initialize: error = %+v", resp.Error) } - result, _ := resp.Result.(map[string]any) - caps, _ := result["capabilities"].(map[string]any) - if _, ok := caps["tools"]; !ok { + result, _ := resp.Result.(agentcapabilities.MCPInitializeResult) + if result.Capabilities.Tools == nil { t.Fatal("initialize must advertise tools capability so MCP clients enumerate the tool surface") } - info, _ := result["serverInfo"].(map[string]any) - if info["name"] != "pulse-mcp" { - t.Errorf("serverInfo.name = %v, want pulse-mcp", info["name"]) + if !strings.Contains(result.Instructions, "governed infrastructure-operations surface") { + t.Fatalf("initialize must include shared Pulse Intelligence operating instructions, got %q", result.Instructions) + } + if result.ServerInfo.Name != "pulse-mcp" { + t.Errorf("serverInfo.name = %v, want pulse-mcp", result.ServerInfo.Name) + } + expected := agentcapabilities.NewMCPToolServerInitializeResult(pulseMCPServerName, pulseMCPServerVersion, false) + if result.ProtocolVersion != expected.ProtocolVersion || result.ServerInfo != expected.ServerInfo { + t.Fatalf("initialize result must use shared MCP constructor; got %+v want %+v", result, expected) } } -// TestServer_ToolsListProjectsManifestSkippingSubscribeEvents -// pins the auto-generation rule: tools/list must surface every -// manifest capability except subscribe_events (which is a stream, -// not a tool). Adding a capability to the manifest must -// automatically make it visible to MCP clients without changes -// here. -func TestServer_ToolsListProjectsManifestSkippingSubscribeEvents(t *testing.T) { +func TestServer_ResourcesListAndReadProjectContextCapabilities(t *testing.T) { + type captured struct { + paths []string + token string + } + var got captured + mu := sync.Mutex{} + pulse := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + defer mu.Unlock() + got.paths = append(got.paths, r.URL.EscapedPath()) + got.token = r.Header.Get(agentcapabilities.AgentAPITokenHeader) + + w.Header().Set("Content-Type", "application/json") + switch r.URL.EscapedPath() { + case "/api/agent/fleet-context": + _, _ = w.Write([]byte(`{ + "resources": [ + { + "canonicalId": "vm:101", + "resourceType": "virtual-machine", + "resourceName": "Database VM", + "technology": "proxmox", + "pendingApprovalCount": 1, + "findings": { "total": 2, "critical": 1, "warning": 1, "info": 0 } + } + ] + }`)) + case "/api/agent/resource-context/vm%3A101": + _, _ = w.Write([]byte(`{"canonicalId":"vm:101","resourceName":"Database VM"}`)) + default: + http.NotFound(w, r) + } + })) + defer pulse.Close() + + s := &mcpServer{ + baseURL: pulse.URL, + token: "resource-test-token", + manifest: &agentCapabilitiesManifest{ + Version: "v1", + Capabilities: []agentCapability{ + {Name: agentcapabilities.FleetContextCapabilityName, Path: "/api/agent/fleet-context", Method: http.MethodGet, Description: "triage"}, + {Name: agentcapabilities.ResourceContextCapabilityName, Path: "/api/agent/resource-context/{resourceId}", Method: http.MethodGet, Description: "depth"}, + }, + }, + http: pulse.Client(), + } + + init := s.dispatch(context.Background(), &jsonRPCRequest{ + JSONRPC: "2.0", + ID: json.RawMessage(`1`), + Method: agentcapabilities.MCPMethodInitialize, + }) + if init.Error != nil { + t.Fatalf("initialize: error = %+v", init.Error) + } + initResult, ok := init.Result.(agentcapabilities.MCPInitializeResult) + if !ok { + t.Fatalf("initialize result type = %T, want shared MCPInitializeResult", init.Result) + } + if initResult.Capabilities.Resources == nil { + t.Fatalf("initialize must advertise resources when both context capabilities exist: %+v", initResult.Capabilities) + } + + list := s.dispatch(context.Background(), &jsonRPCRequest{ + JSONRPC: "2.0", + ID: json.RawMessage(`2`), + Method: agentcapabilities.MCPMethodResourcesList, + }) + if list.Error != nil { + t.Fatalf("resources/list: error = %+v", list.Error) + } + listResult, ok := list.Result.(agentcapabilities.MCPListResourcesResult) + if !ok { + t.Fatalf("resources/list result type = %T, want shared MCPListResourcesResult", list.Result) + } + if len(listResult.Resources) != 1 { + t.Fatalf("resources/list returned %+v, want one resource", listResult.Resources) + } + resource := listResult.Resources[0] + if resource.URI != agentcapabilities.MCPResourceURI("vm:101") || resource.Name != "Database VM" || resource.MimeType != agentcapabilities.MCPResourceContextMIMEType { + t.Fatalf("projected MCP resource = %+v", resource) + } + if !strings.Contains(resource.Description, "virtual-machine") || !strings.Contains(resource.Description, "pending approvals: 1") { + t.Fatalf("projected resource description must carry fleet context summary; got %q", resource.Description) + } + + params, _ := json.Marshal(agentcapabilities.MCPReadResourceParams{URI: resource.URI}) + read := s.dispatch(context.Background(), &jsonRPCRequest{ + JSONRPC: "2.0", + ID: json.RawMessage(`3`), + Method: agentcapabilities.MCPMethodResourcesRead, + Params: params, + }) + if read.Error != nil { + t.Fatalf("resources/read: error = %+v", read.Error) + } + readResult, ok := read.Result.(agentcapabilities.MCPReadResourceResult) + if !ok { + t.Fatalf("resources/read result type = %T, want shared MCPReadResourceResult", read.Result) + } + if len(readResult.Contents) != 1 { + t.Fatalf("resources/read contents = %+v, want one JSON content block", readResult.Contents) + } + content := readResult.Contents[0] + if content.URI != resource.URI || content.MimeType != agentcapabilities.MCPResourceContextMIMEType { + t.Fatalf("resources/read content = %+v", content) + } + if content.Text != `{"canonicalId":"vm:101","resourceName":"Database VM"}` { + t.Fatalf("resources/read content text = %q", content.Text) + } + + mu.Lock() + defer mu.Unlock() + if got.token != "resource-test-token" { + t.Fatalf("upstream token = %q, want resource-test-token", got.token) + } + if strings.Join(got.paths, ",") != "/api/agent/fleet-context,/api/agent/resource-context/vm%3A101" { + t.Fatalf("upstream paths = %+v", got.paths) + } +} + +func TestServer_PromptsListAndGetProjectManifestWorkflowPrompts(t *testing.T) { + s := &mcpServer{ + manifest: &agentCapabilitiesManifest{ + Version: "v1", + Capabilities: []agentCapability{ + {Name: agentcapabilities.FleetContextCapabilityName, Path: "/api/agent/fleet-context", Method: http.MethodGet, Description: "triage"}, + {Name: agentcapabilities.ResourceContextCapabilityName, Path: "/api/agent/resource-context/{resourceId}", Method: http.MethodGet, Description: "depth"}, + {Name: "list_findings", Path: "/api/ai/patrol/findings", Method: http.MethodGet, Description: "findings"}, + }, + }, + } + + init := s.dispatch(context.Background(), &jsonRPCRequest{ + JSONRPC: "2.0", + ID: json.RawMessage(`1`), + Method: agentcapabilities.MCPMethodInitialize, + }) + if init.Error != nil { + t.Fatalf("initialize: error = %+v", init.Error) + } + initResult, ok := init.Result.(agentcapabilities.MCPInitializeResult) + if !ok { + t.Fatalf("initialize result type = %T, want shared MCPInitializeResult", init.Result) + } + if initResult.Capabilities.Prompts == nil { + t.Fatalf("initialize must advertise prompts when workflow capabilities exist: %+v", initResult.Capabilities) + } + + list := s.dispatch(context.Background(), &jsonRPCRequest{ + JSONRPC: "2.0", + ID: json.RawMessage(`2`), + Method: agentcapabilities.MCPMethodPromptsList, + }) + if list.Error != nil { + t.Fatalf("prompts/list: error = %+v", list.Error) + } + listResult, ok := list.Result.(agentcapabilities.MCPListPromptsResult) + if !ok { + t.Fatalf("prompts/list result type = %T, want shared MCPListPromptsResult", list.Result) + } + if len(listResult.Prompts) != 3 { + t.Fatalf("prompts/list returned %+v, want three prompts", listResult.Prompts) + } + + params, _ := json.Marshal(agentcapabilities.MCPGetPromptParams{ + Name: agentcapabilities.MCPPromptReviewFinding, + Arguments: map[string]string{"finding_id": "finding-1"}, + }) + get := s.dispatch(context.Background(), &jsonRPCRequest{ + JSONRPC: "2.0", + ID: json.RawMessage(`3`), + Method: agentcapabilities.MCPMethodPromptsGet, + Params: params, + }) + if get.Error != nil { + t.Fatalf("prompts/get: error = %+v", get.Error) + } + getResult, ok := get.Result.(agentcapabilities.MCPGetPromptResult) + if !ok { + t.Fatalf("prompts/get result type = %T, want shared MCPGetPromptResult", get.Result) + } + if len(getResult.Messages) != 1 || getResult.Messages[0].Role != "user" { + t.Fatalf("prompts/get messages = %+v", getResult.Messages) + } + if !strings.Contains(getResult.Messages[0].Content.Text, `"finding-1"`) || !strings.Contains(getResult.Messages[0].Content.Text, "list_findings") { + t.Fatalf("finding prompt text = %q", getResult.Messages[0].Content.Text) + } +} + +func TestServer_PromptsGetRecordsWorkflowPromptActivity(t *testing.T) { + var got struct { + path string + token string + surface string + name string + } + client := httpDoerFunc(func(r *http.Request) (*http.Response, error) { + got.path = r.URL.Path + got.token = r.Header.Get(agentcapabilities.AgentAPITokenHeader) + got.surface = r.Header.Get(agentcapabilities.AgentSurfaceHeader) + var payload map[string]string + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + t.Fatalf("decode activity payload: %v", err) + } + got.name = payload["name"] + return &http.Response{ + StatusCode: http.StatusNoContent, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader("")), + Request: r, + }, nil + }) + + s := &mcpServer{ + baseURL: "http://pulse.local", + token: "test-token", + http: client, + manifest: &agentCapabilitiesManifest{ + Version: "v1", + WorkflowPrompts: []agentcapabilities.PulseWorkflowPrompt{{ + Name: agentcapabilities.PulseWorkflowPromptOperationsLoop, + Label: "Ask Patrol to handle an issue", + Description: "Have Patrol investigate, follow policy, take approved actions, verify, and record the result.", + }}, + }, + } + params, _ := json.Marshal(agentcapabilities.MCPGetPromptParams{Name: agentcapabilities.MCPPromptOperationsLoop}) + resp := s.dispatch(context.Background(), &jsonRPCRequest{ + JSONRPC: "2.0", + ID: json.RawMessage(`1`), + Method: agentcapabilities.MCPMethodPromptsGet, + Params: params, + }) + if resp.Error != nil { + t.Fatalf("prompts/get: error = %+v", resp.Error) + } + + if got.path != agentcapabilities.AgentWorkflowPromptActivityPath { + t.Fatalf("activity path = %q, want %q", got.path, agentcapabilities.AgentWorkflowPromptActivityPath) + } + if got.token != "test-token" { + t.Fatalf("%s = %q, want test-token", agentcapabilities.AgentAPITokenHeader, got.token) + } + if got.surface != agentcapabilities.AgentSurfacePulseMCP { + t.Fatalf("%s = %q, want %s", agentcapabilities.AgentSurfaceHeader, got.surface, agentcapabilities.AgentSurfacePulseMCP) + } + if got.name != agentcapabilities.PulseWorkflowPromptOperationsLoop { + t.Fatalf("activity prompt name = %q, want %s", got.name, agentcapabilities.PulseWorkflowPromptOperationsLoop) + } +} + +func TestServerDispatchUsesSharedMCPDispatcher(t *testing.T) { + src, err := os.ReadFile("main.go") + if err != nil { + t.Fatalf("read main.go: %v", err) + } + text := string(src) + + if !strings.Contains(text, "agentcapabilities.DispatchMCPToolServerRequest(") { + t.Fatal("pulse-mcp dispatch must delegate MCP method semantics to agentcapabilities") + } + for _, required := range []string{ + "s.manifestToolServer().Handlers(func()", + "agentcapabilities.MCPManifestToolServer{", + "ServerName: pulseMCPServerName", + "SurfaceID: agentcapabilities.SurfaceIDPulseMCP", + "Manifest: manifest", + } { + if !strings.Contains(text, required) { + t.Fatalf("pulse-mcp must project manifest-backed tool semantics through agentcapabilities; missing %s", required) + } + } + for _, required := range []string{ + "agentcapabilities.ServeJSONRPCLines(", + "agentcapabilities.WriteJSONRPCMessage(out, v)", + "agentcapabilities.StreamMCPEventNotifications(", + } { + if !strings.Contains(text, required) { + t.Fatalf("pulse-mcp must delegate JSON-RPC request handling to agentcapabilities; missing %s", required) + } + } + for _, forbidden := range []string{ + "bufio.NewScanner(in)", + "scanner.Scan()", + "agentcapabilities.DecodeJSONRPCRequest(", + "agentcapabilities.NewJSONRPCParseErrorResponse(", + "agentcapabilities.JSONRPCRequestExpectsResponse(", + "agentcapabilities.StreamAgentSSERecords(", + "agentcapabilities.NewMCPEventNotification(", + "func (s *mcpServer) maybeEmitNotification(", + "json.Unmarshal([]byte(line)", + "agentcapabilities.NewJSONRPCErrorResponse(\n\t\t\t\tnil", + "agentcapabilities.JSONRPCErrorParse", + "len(req.ID) == 0", + "string(req.ID) == \"null\"", + "switch req.Method", + "type jsonRPCError = agentcapabilities.JSONRPCError", + "agentcapabilities.MCPToolServerHandlers{", + "agentcapabilities.NewJSONRPCResponse(req.ID, nil)", + "agentcapabilities.JSONRPCErrorInternal", + "agentcapabilities.JSONRPCErrorMethodNotFound", + "func (s *mcpServer) handleInitialize(", + "func (s *mcpServer) handleToolsList(", + "func (s *mcpServer) handleToolsCall(", + "agentcapabilities.NewMCPToolServerInitializeResult(pulseMCPServerName", + "agentcapabilities.ProjectTools(s.manifest.Capabilities)", + "agentcapabilities.ExecuteMCPToolHTTP(", + "agentcapabilities.ExecuteMCPManifestSurfaceToolHTTP(ctx, s.http, s.baseURL, s.token, s.manifest", + } { + if strings.Contains(text, forbidden) { + t.Fatalf("pulse-mcp must not own MCP method dispatch or error mapping; found %s", forbidden) + } + } +} + +// TestServer_ToolsListProjectsPulseMCPSurfaceContract pins the auto-generation +// rule: tools/list must surface the manifest-owned Pulse MCP tool contract, +// not every raw manifest capability. Streaming capabilities and capabilities +// omitted from the Pulse MCP contract stay out of the request/response tool +// surface. +func TestServer_ToolsListProjectsPulseMCPSurfaceContract(t *testing.T) { s := &mcpServer{manifest: &agentCapabilitiesManifest{ - Version: "v1", + Version: "v1", + SurfaceContract: agentcapabilities.CanonicalManifest().SurfaceContract, + SurfaceToolContracts: testPulseMCPSurfaceToolContracts(agentcapabilities.ResourceContextCapabilityName, agentcapabilities.SetOperatorStateCapabilityName), Capabilities: []agentCapability{ - {Name: "get_resource_context", Path: "/api/agent/resource-context/{resourceId}", Method: http.MethodGet, Description: "depth"}, - {Name: "get_fleet_context", Path: "/api/agent/fleet-context", Method: http.MethodGet, Description: "triage"}, - {Name: "subscribe_events", Path: "/api/agent/events", Method: http.MethodGet, Description: "stream"}, - {Name: "set_operator_state", Path: "/api/resources/{resourceId}/operator-state", Method: http.MethodPut, Description: "write intent"}, + {Name: "get_resource_context", Title: "Inspect resource", Path: "/api/agent/resource-context/{resourceId}", Method: http.MethodGet, Description: "depth", ActionMode: agentcapabilities.ActionModeRead, OutputSchema: json.RawMessage(`{"type":"object","properties":{"canonicalId":{"type":"string"}}}`)}, + {Name: "get_fleet_context", Title: "Triage fleet", Path: "/api/agent/fleet-context", Method: http.MethodGet, Description: "triage", ActionMode: agentcapabilities.ActionModeRead}, + {Name: "subscribe_events", Title: "Subscribe events", Path: "/api/agent/events", Method: http.MethodGet, Description: "stream", ActionMode: agentcapabilities.ActionModeRead}, + {Name: agentcapabilities.SetOperatorStateCapabilityName, Title: "Set operator state", Path: agentcapabilities.OperatorStateCapabilityPath, Method: http.MethodPut, Scope: "monitoring:write", Description: "write intent", ActionMode: agentcapabilities.ActionModeWrite}, }, }} resp := s.dispatch(context.Background(), &jsonRPCRequest{ @@ -208,19 +786,177 @@ func TestServer_ToolsListProjectsManifestSkippingSubscribeEvents(t *testing.T) { if resp.Error != nil { t.Fatalf("tools/list: error = %+v", resp.Error) } - result, _ := resp.Result.(map[string]any) - tools, _ := result["tools"].([]mcpTool) + result, _ := resp.Result.(agentcapabilities.MCPProjectedToolsResult) + tools := result.Tools + if len(tools) != 2 { + t.Fatalf("tools/list len = %d, want 2 Pulse MCP surface tools", len(tools)) + } names := map[string]bool{} + titles := map[string]string{} + descriptions := map[string]string{} + outputSchemas := map[string]json.RawMessage{} + annotations := map[string]*agentcapabilities.MCPToolAnnotations{} + metadata := map[string]map[string]any{} for _, tool := range tools { names[tool.Name] = true + titles[tool.Name] = tool.Title + descriptions[tool.Name] = tool.Description + outputSchemas[tool.Name] = tool.OutputSchema + annotations[tool.Name] = tool.Annotations + if meta, ok := tool.Meta[agentcapabilities.ToolMetaPulseCapabilityKey].(map[string]any); ok { + metadata[tool.Name] = meta + } } - for _, want := range []string{"get_resource_context", "get_fleet_context", "set_operator_state"} { + for _, want := range []string{agentcapabilities.ResourceContextCapabilityName, agentcapabilities.SetOperatorStateCapabilityName} { if !names[want] { t.Errorf("tools/list missing %q", want) } } + if names[agentcapabilities.FleetContextCapabilityName] { + t.Errorf("%s must not be exposed when omitted from the Pulse MCP surface contract", agentcapabilities.FleetContextCapabilityName) + } + if !strings.Contains(descriptions[agentcapabilities.SetOperatorStateCapabilityName], "write intent") { + t.Errorf("tools/list must preserve manifest description; got %q", descriptions[agentcapabilities.SetOperatorStateCapabilityName]) + } + if titles[agentcapabilities.ResourceContextCapabilityName] != "Inspect resource" { + t.Errorf("tools/list must project manifest title; got %q", titles[agentcapabilities.ResourceContextCapabilityName]) + } + if !strings.Contains(string(outputSchemas[agentcapabilities.ResourceContextCapabilityName]), `"canonicalId"`) { + t.Errorf("tools/list must project manifest outputSchema; got %s", string(outputSchemas[agentcapabilities.ResourceContextCapabilityName])) + } + if !strings.Contains(descriptions[agentcapabilities.SetOperatorStateCapabilityName], "required scope: ") { + t.Errorf("tools/list must project manifest capability metadata; got %q", descriptions[agentcapabilities.SetOperatorStateCapabilityName]) + } + setOperatorMeta := metadata[agentcapabilities.SetOperatorStateCapabilityName] + if setOperatorMeta["scope"] != "monitoring:write" { + t.Errorf("tools/list must project structured Pulse scope metadata; got %#v", setOperatorMeta) + } + route, _ := setOperatorMeta["route"].(map[string]any) + if route["method"] != http.MethodPut || route["path"] != agentcapabilities.OperatorStateCapabilityPath { + t.Errorf("tools/list must project structured Pulse route metadata; got %#v", route) + } + governance, _ := setOperatorMeta["governance"].(map[string]any) + if governance["actionMode"] != string(agentcapabilities.ActionModeWrite) { + t.Errorf("tools/list must project structured Pulse governance metadata; got %#v", governance) + } + assertMCPToolAnnotations(t, annotations[agentcapabilities.ResourceContextCapabilityName], true, false, true, true) + assertMCPToolAnnotations(t, annotations[agentcapabilities.SetOperatorStateCapabilityName], false, true, false, true) if names["subscribe_events"] { - t.Error("subscribe_events must NOT be exposed as an MCP tool — SSE streams don't fit the request/response shape; future slices can layer notifications") + t.Error("subscribe_events must NOT be exposed as an MCP tool — SSE streams don't fit the request/response shape") + } +} + +func assertMCPToolAnnotations(t *testing.T, annotations *agentcapabilities.MCPToolAnnotations, readOnly, destructive, idempotent, openWorld bool) { + t.Helper() + if annotations == nil { + t.Fatal("tool annotations are nil") + } + assertMCPBoolRef(t, "readOnlyHint", annotations.ReadOnlyHint, readOnly) + assertMCPBoolRef(t, "destructiveHint", annotations.DestructiveHint, destructive) + assertMCPBoolRef(t, "idempotentHint", annotations.IdempotentHint, idempotent) + assertMCPBoolRef(t, "openWorldHint", annotations.OpenWorldHint, openWorld) +} + +func assertMCPBoolRef(t *testing.T, name string, got *bool, want bool) { + t.Helper() + if got == nil { + t.Fatalf("%s is nil, want %v", name, want) + } + if *got != want { + t.Fatalf("%s = %v, want %v", name, *got, want) + } +} + +func TestFormatToolDescriptionProjectsCapabilityMetadata(t *testing.T) { + desc := agentcapabilities.ToolDescription(agentCapability{ + Name: "plan_action", + Description: "Plan an action against a resource.", + Category: "action", + Method: http.MethodPost, + Path: "/api/actions/plan", + Scope: "ai:execute", + ActionMode: "write", + ApprovalPolicy: "action_plan", + RequestBodyShape: "ActionRequest", + ResponseShape: "ActionPlan", + ErrorCodes: []string{"invalid_action_request", "resource_not_found"}, + }) + + for _, want := range []string{ + "Plan an action against a resource.", + "Pulse capability metadata:", + "category: action", + "route: POST /api/actions/plan", + "required scope: ai:execute", + "action mode: write", + "approval policy: action_plan", + "request body: ActionRequest", + "response: ActionPlan", + "stable error codes: invalid_action_request, resource_not_found", + } { + if !strings.Contains(desc, want) { + t.Fatalf("ToolDescription missing %q in %q", want, desc) + } + } +} + +func TestServer_ToolsCallRejectsUnknownManifestCapability(t *testing.T) { + s := &mcpServer{manifest: &agentCapabilitiesManifest{ + Version: "v1", + Capabilities: []agentCapability{ + {Name: "get_fleet_context", Path: "/api/agent/fleet-context", Method: http.MethodGet}, + }, + }} + + params, _ := json.Marshal(map[string]any{ + "name": "missing_capability", + "arguments": map[string]any{}, + }) + resp := s.dispatch(context.Background(), &jsonRPCRequest{ + JSONRPC: "2.0", + ID: json.RawMessage(`1`), + Method: "tools/call", + Params: params, + }) + + if resp.Error == nil { + t.Fatal("unknown manifest capability must produce a JSON-RPC error") + } + if !strings.Contains(resp.Error.Message, "unknown tool: missing_capability") { + t.Fatalf("unknown capability error = %q, want stable unknown tool message", resp.Error.Message) + } +} + +func TestServer_ToolsCallRejectsMalformedParamsWithSharedDecodeMessage(t *testing.T) { + s := &mcpServer{manifest: &agentCapabilitiesManifest{Version: "v1"}} + resp := s.dispatch(context.Background(), &jsonRPCRequest{ + JSONRPC: "2.0", + ID: json.RawMessage(`1`), + Method: "tools/call", + Params: json.RawMessage(`{"name":`), + }) + + if resp.Error == nil { + t.Fatal("malformed tools/call params must produce a JSON-RPC error") + } + if resp.Error.Code != agentcapabilities.JSONRPCErrorInternal { + t.Fatalf("error code = %d, want JSONRPCErrorInternal", resp.Error.Code) + } + if !strings.Contains(resp.Error.Message, "decode tools/call params") { + t.Fatalf("decode error = %q, want shared decode message", resp.Error.Message) + } + + resp = s.dispatch(context.Background(), &jsonRPCRequest{ + JSONRPC: "2.0", + ID: json.RawMessage(`2`), + Method: "tools/call", + Params: json.RawMessage(`{"name":" ","arguments":{}}`), + }) + if resp.Error == nil { + t.Fatal("invalid tools/call params must produce a JSON-RPC error") + } + if !strings.Contains(resp.Error.Message, "decode tools/call params: tool name is required") { + t.Fatalf("invalid params error = %q, want shared validation message", resp.Error.Message) } } @@ -232,34 +968,41 @@ func TestServer_ToolsListProjectsManifestSkippingSubscribeEvents(t *testing.T) { // would on the wire. func TestServer_ToolsCallProxiesToPulseAndPreservesErrorEnvelope(t *testing.T) { type captured struct { - method string - path string - token string - body string + method string + path string + token string + surface string + body string } var got captured mu := sync.Mutex{} - pulse := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + const errorBody = `{"error":"resource_not_found","message":"No resource is registered with this canonical id."}` + client := httpDoerFunc(func(r *http.Request) (*http.Response, error) { mu.Lock() defer mu.Unlock() got.method = r.Method got.path = r.URL.Path got.token = r.Header.Get("X-API-Token") + got.surface = r.Header.Get(agentcapabilities.AgentSurfaceHeader) if r.Body != nil { b, _ := io.ReadAll(r.Body) got.body = string(b) } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusNotFound) - _, _ = w.Write([]byte(`{"error":"resource_not_found","message":"No resource is registered with this canonical id."}`)) - })) - defer pulse.Close() + return &http.Response{ + StatusCode: http.StatusNotFound, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(errorBody)), + Request: r, + }, nil + }) s := &mcpServer{ - baseURL: pulse.URL, + baseURL: "http://pulse.test", token: "test-token", manifest: &agentCapabilitiesManifest{ - Version: "v1", + Version: "v1", + SurfaceContract: agentcapabilities.CanonicalManifest().SurfaceContract, + SurfaceToolContracts: testPulseMCPSurfaceToolContracts(agentcapabilities.ResourceContextCapabilityName), Capabilities: []agentCapability{ { Name: "get_resource_context", @@ -269,7 +1012,7 @@ func TestServer_ToolsCallProxiesToPulseAndPreservesErrorEnvelope(t *testing.T) { }, }, }, - http: pulse.Client(), + http: client, } params, _ := json.Marshal(map[string]any{ @@ -295,17 +1038,34 @@ func TestServer_ToolsCallProxiesToPulseAndPreservesErrorEnvelope(t *testing.T) { if got.token != "test-token" { t.Errorf("upstream token header = %q, want test-token", got.token) } + if got.surface != agentcapabilities.AgentSurfacePulseMCP { + t.Errorf("upstream agent surface header = %q, want %q", got.surface, agentcapabilities.AgentSurfacePulseMCP) + } - result, _ := resp.Result.(map[string]any) - if result["isError"] != true { - t.Errorf("non-2xx upstream must surface as MCP isError=true; got %v", result["isError"]) + result, ok := resp.Result.(agentcapabilities.MCPToolResult) + if !ok { + t.Fatalf("tools/call result type = %T, want shared MCPToolResult", resp.Result) } - content, _ := result["content"].([]map[string]any) - if len(content) != 1 { - t.Fatalf("content len = %d, want 1", len(content)) + if !result.IsError { + t.Errorf("non-2xx upstream must surface as MCP isError=true; got %v", result.IsError) } - if !strings.Contains(content[0]["text"].(string), `"error":"resource_not_found"`) { - t.Errorf("MCP content must preserve substrate error envelope verbatim; got %v", content[0]["text"]) + if len(result.Content) != 1 { + t.Fatalf("content len = %d, want 1", len(result.Content)) + } + expected := agentcapabilities.NewCapabilityHTTPToolResult(agentcapabilities.HTTPCallResponse{ + Method: http.MethodGet, + Path: "/api/agent/resource-context/vm:does-not-exist", + StatusCode: http.StatusNotFound, + Body: []byte(errorBody), + }) + if result.IsError != expected.IsError || result.Content[0].Text != expected.Content[0].Text { + t.Fatalf("tools/call result must use shared HTTP-to-MCP result semantics; got %+v want %+v", result, expected) + } + if result.StructuredContent["error"] != "resource_not_found" { + t.Fatalf("tools/call structuredContent = %+v, want resource_not_found error", result.StructuredContent) + } + if !strings.Contains(result.Content[0].Text, `"error":"resource_not_found"`) { + t.Errorf("MCP content must preserve substrate error envelope verbatim; got %v", result.Content[0].Text) } } @@ -349,30 +1109,31 @@ func TestServer_NotificationGetsNoResponse(t *testing.T) { func TestServer_InitializeAdvertisesNotificationsCapabilityWhenEnabled(t *testing.T) { t.Run("disabled by default", func(t *testing.T) { s := &mcpServer{manifest: &agentCapabilitiesManifest{}} - result := s.handleInitialize().(map[string]any) - caps := result["capabilities"].(map[string]any) - if _, ok := caps["experimental"]; ok { + result := s.manifestToolServer().Initialize() + if len(result.Capabilities.Experimental) > 0 { t.Error("initialize must NOT advertise pulseNotifications when --emit-notifications is off") } }) t.Run("advertised when enabled", func(t *testing.T) { s := &mcpServer{manifest: &agentCapabilitiesManifest{}, emitNotifications: true} - result := s.handleInitialize().(map[string]any) - caps := result["capabilities"].(map[string]any) - exp, ok := caps["experimental"].(map[string]any) - if !ok { + result := s.manifestToolServer().Initialize() + exp := result.Capabilities.Experimental + if len(exp) == 0 { t.Fatal("initialize must advertise experimental block when --emit-notifications is on") } - pn, ok := exp["pulseNotifications"].(map[string]any) + pn, ok := exp[agentcapabilities.MCPPulseNotificationsExperimentalKey].(agentcapabilities.MCPPulseNotificationsCapability) if !ok { t.Fatal("experimental block must contain pulseNotifications descriptor") } - kinds, ok := pn["kinds"].([]string) - if !ok || len(kinds) == 0 { - t.Fatalf("pulseNotifications.kinds must list the SSE event kinds; got %v", pn["kinds"]) + kinds := pn.Kinds + if len(kinds) == 0 { + t.Fatalf("pulseNotifications.kinds must list the SSE event kinds; got %v", pn.Kinds) + } + want := map[string]bool{} + for _, kind := range agentcapabilities.AgentActionableEventKinds() { + want[kind] = false } - want := map[string]bool{"finding.created": false, "approval.pending": false, "action.completed": false} for _, k := range kinds { if _, exists := want[k]; exists { want[k] = true @@ -386,58 +1147,6 @@ func TestServer_InitializeAdvertisesNotificationsCapabilityWhenEnabled(t *testin }) } -// TestServer_MaybeEmitNotificationFiltersTransportEvents pins that -// the bridge skips events that are pure transport plumbing -// (stream.connected, heartbeat). Forwarding those would surface -// noise an agent has no useful action on, and would teach -// downstream code to filter them client-side instead of trusting -// the substrate's "doorbell, not transport" intent. -func TestServer_MaybeEmitNotificationFiltersTransportEvents(t *testing.T) { - cases := []struct { - name string - event, data string - shouldEmit bool - methodPrefix string - }{ - {"finding.created passes through", "finding.created", `{"findingId":"f1"}`, true, "notifications/finding.created"}, - {"approval.pending passes through", "approval.pending", `{"approvalId":"a1"}`, true, "notifications/approval.pending"}, - {"action.completed passes through", "action.completed", `{"actionId":"x1"}`, true, "notifications/action.completed"}, - {"stream.connected is filtered", "stream.connected", `{}`, false, ""}, - {"heartbeat is filtered", "heartbeat", "", false, ""}, - {"empty event is filtered", "", `{"x":1}`, false, ""}, - {"empty data is filtered", "finding.created", "", false, ""}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - out := &bytes.Buffer{} - s := &mcpServer{out: out} - s.maybeEmitNotification(tc.event, tc.data) - if tc.shouldEmit { - if out.Len() == 0 { - t.Fatalf("expected notification on stdout for %q; got nothing", tc.event) - } - body := out.String() - if !strings.Contains(body, `"method":"`+tc.methodPrefix+`"`) { - t.Errorf("expected method %q; got %s", tc.methodPrefix, body) - } - if !strings.Contains(body, `"jsonrpc":"2.0"`) { - t.Errorf("notification must be JSON-RPC 2.0; got %s", body) - } - // Notifications must NOT carry an id field per - // JSON-RPC 2.0 spec — clients that see an id treat - // the message as a request and may try to respond. - if strings.Contains(body, `"id":`) { - t.Errorf("notification must omit the id field; got %s", body) - } - } else { - if out.Len() != 0 { - t.Errorf("expected silence for %q event; got %s", tc.event, out.String()) - } - } - }) - } -} - // TestServer_StreamSSEOnceTranslatesEventsToNotifications is the // integration test for the bridge: spin up a fake SSE server // emitting the substrate's wire format, point the consumer at it, @@ -445,24 +1154,31 @@ func TestServer_MaybeEmitNotificationFiltersTransportEvents(t *testing.T) { // notification on the configured out writer. func TestServer_StreamSSEOnceTranslatesEventsToNotifications(t *testing.T) { sseBody := strings.Join([]string{ - "event: stream.connected", + ": sync comment uses standard SSE comment framing", + "event: " + string(agentcapabilities.EventKindStreamConnected), "data: {}", "", - "event: finding.created", + "event: " + string(agentcapabilities.EventKindFindingCreated), "data: {\"findingId\":\"f1\",\"severity\":\"critical\"}", "", - "event: heartbeat", + "event: " + string(agentcapabilities.EventKindHeartbeat), "", - "event: action.completed", + "event: " + string(agentcapabilities.EventKindActionCompleted), "data: {\"actionId\":\"x1\",\"success\":true,\"verification\":{\"ran\":true,\"success\":true,\"commandRedacted\":true}}", "", - }, "\n") + "\n" + }, "\r\n") + "\r\n" pulse := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("X-API-Token") != "test-token" { + if r.Header.Get(agentcapabilities.AgentAPITokenHeader) != "test-token" { w.WriteHeader(http.StatusUnauthorized) return } + if r.Header.Get("Accept") != agentcapabilities.AgentSSEAccept { + t.Errorf("Accept header = %q, want %q", r.Header.Get("Accept"), agentcapabilities.AgentSSEAccept) + } + if r.Header.Get(agentcapabilities.AgentSurfaceHeader) != agentcapabilities.AgentSurfacePulseMCP { + t.Errorf("%s header = %q, want %q", agentcapabilities.AgentSurfaceHeader, r.Header.Get(agentcapabilities.AgentSurfaceHeader), agentcapabilities.AgentSurfacePulseMCP) + } w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(sseBody)) })) @@ -475,7 +1191,7 @@ func TestServer_StreamSSEOnceTranslatesEventsToNotifications(t *testing.T) { http: pulse.Client(), out: out, } - if err := s.streamSSEOnce(context.Background(), pulse.URL+"/api/agent/events"); err != nil { + if err := s.streamSSEOnce(context.Background(), agentcapabilities.AgentEventsPath); err != nil { t.Fatalf("streamSSEOnce: %v", err) } @@ -489,7 +1205,7 @@ func TestServer_StreamSSEOnceTranslatesEventsToNotifications(t *testing.T) { if !strings.Contains(body, `"verification":{"ran":true,"success":true,"commandRedacted":true}`) { t.Errorf("action.completed verification must round-trip through MCP notification params; got %s", body) } - if strings.Contains(body, "stream.connected") { + if strings.Contains(body, string(agentcapabilities.EventKindStreamConnected)) { t.Errorf("stream.connected must be filtered out as transport plumbing; got %s", body) } if strings.Contains(body, `"method":"notifications/heartbeat"`) { @@ -546,11 +1262,13 @@ func TestServer_ToolsCallSendsPutBodyForWriteCapabilities(t *testing.T) { baseURL: pulse.URL, token: "write-test-token", manifest: &agentCapabilitiesManifest{ - Version: "v1", + Version: "v1", + SurfaceContract: agentcapabilities.CanonicalManifest().SurfaceContract, + SurfaceToolContracts: testPulseMCPSurfaceToolContracts(agentcapabilities.SetOperatorStateCapabilityName), Capabilities: []agentCapability{ { - Name: "set_operator_state", - Path: "/api/resources/{resourceId}/operator-state", + Name: agentcapabilities.SetOperatorStateCapabilityName, + Path: agentcapabilities.OperatorStateCapabilityPath, Method: http.MethodPut, Scope: "monitoring:write", RequestBodyShape: "ResourceOperatorStateInput", @@ -562,7 +1280,7 @@ func TestServer_ToolsCallSendsPutBodyForWriteCapabilities(t *testing.T) { } params, _ := json.Marshal(map[string]any{ - "name": "set_operator_state", + "name": agentcapabilities.SetOperatorStateCapabilityName, "arguments": map[string]any{ "resourceId": "vm:101", "body": map[string]any{ @@ -610,20 +1328,26 @@ func TestServer_ToolsCallSendsPutBodyForWriteCapabilities(t *testing.T) { t.Errorf("upstream body must round-trip note verbatim; got %v", sentBody["note"]) } - // MCP result shape: 2xx upstream becomes isError=false with - // the upstream body in a text content block. - result, _ := resp.Result.(map[string]any) - if result["isError"] != false { - t.Errorf("2xx upstream must surface as isError=false; got %v", result["isError"]) + // MCP result shape: 2xx upstream becomes isError=false with the upstream + // body in a text content block plus shared structuredContent for clients + // that can branch on machine-readable tool output. + result, ok := resp.Result.(agentcapabilities.MCPToolResult) + if !ok { + t.Fatalf("tools/call result type = %T, want shared MCPToolResult", resp.Result) } - content, _ := result["content"].([]map[string]any) - if len(content) != 1 { - t.Fatalf("content len = %d, want 1", len(content)) + if result.IsError { + t.Errorf("2xx upstream must surface as isError=false; got %v", result.IsError) } - text, _ := content[0]["text"].(string) + if len(result.Content) != 1 { + t.Fatalf("content len = %d, want 1", len(result.Content)) + } + text := result.Content[0].Text if !strings.Contains(text, `"canonicalId":"vm:101"`) { t.Errorf("MCP content must carry upstream response body; got %q", text) } + if result.StructuredContent["canonicalId"] != "vm:101" { + t.Fatalf("MCP structuredContent = %+v, want canonicalId vm:101", result.StructuredContent) + } // Server-populated attribution must reach the agent so it // can see WHO the substrate recorded the write under (not the // supplied token, but the resolved actor id). @@ -654,9 +1378,11 @@ func TestServer_ToolsCallTopLevelArgsMakeUpRequestBody(t *testing.T) { baseURL: pulse.URL, token: "test", manifest: &agentCapabilitiesManifest{ + SurfaceContract: agentcapabilities.CanonicalManifest().SurfaceContract, + SurfaceToolContracts: testPulseMCPSurfaceToolContracts(agentcapabilities.AcknowledgeFindingCapabilityName), Capabilities: []agentCapability{ { - Name: "acknowledge_finding", + Name: agentcapabilities.AcknowledgeFindingCapabilityName, Path: "/api/ai/patrol/acknowledge", Method: http.MethodPost, }, @@ -702,12 +1428,14 @@ func TestServer_ToolsCallTopLevelArgsMakeUpRequestBody(t *testing.T) { // canonicalId duplication. func TestServer_ToolsCallTopLevelArgsExcludesPathPlaceholders(t *testing.T) { type captured struct { - path string - body string + path string + requestURI string + body string } var got captured pulse := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { got.path = r.URL.Path + got.requestURI = r.RequestURI b, _ := io.ReadAll(r.Body) got.body = string(b) w.WriteHeader(http.StatusOK) @@ -719,10 +1447,12 @@ func TestServer_ToolsCallTopLevelArgsExcludesPathPlaceholders(t *testing.T) { baseURL: pulse.URL, token: "test", manifest: &agentCapabilitiesManifest{ + SurfaceContract: agentcapabilities.CanonicalManifest().SurfaceContract, + SurfaceToolContracts: testPulseMCPSurfaceToolContracts(agentcapabilities.SetOperatorStateCapabilityName), Capabilities: []agentCapability{ { - Name: "set_operator_state", - Path: "/api/resources/{resourceId}/operator-state", + Name: agentcapabilities.SetOperatorStateCapabilityName, + Path: agentcapabilities.OperatorStateCapabilityPath, Method: http.MethodPut, }, }, @@ -731,9 +1461,9 @@ func TestServer_ToolsCallTopLevelArgsExcludesPathPlaceholders(t *testing.T) { } params, _ := json.Marshal(map[string]any{ - "name": "set_operator_state", + "name": agentcapabilities.SetOperatorStateCapabilityName, "arguments": map[string]any{ - "resourceId": "vm:101", + "resourceId": "vm/special 101", "intentionallyOffline": true, }, }) @@ -747,8 +1477,8 @@ func TestServer_ToolsCallTopLevelArgsExcludesPathPlaceholders(t *testing.T) { t.Fatalf("tools/call: rpc error = %+v", resp.Error) } - if got.path != "/api/resources/vm:101/operator-state" { - t.Errorf("path-placeholder substitution failed; got %q", got.path) + if got.requestURI != "/api/resources/vm%2Fspecial%20101/operator-state" { + t.Errorf("path-placeholder substitution failed; got request URI %q", got.requestURI) } var sent map[string]any if err := json.Unmarshal([]byte(got.body), &sent); err != nil { diff --git a/docs/AGENT_SUBSTRATE.md b/docs/AGENT_SUBSTRATE.md index d59da5056..d337577b9 100644 --- a/docs/AGENT_SUBSTRATE.md +++ b/docs/AGENT_SUBSTRATE.md @@ -8,15 +8,19 @@ what shape this work took. ## What it is Pulse v6 ships an agent-paradigm substrate so external agents (Claude -Desktop, Claude Code, custom MCP clients, plain HTTP consumers) can +Desktop, Claude Code, OpenCode, other MCP clients, plain HTTP consumers) can drive Pulse with the same context an in-process Patrol or Assistant has. The substrate has four axes: -**Discovery.** A hand-authored manifest at `/api/agent/capabilities` +**Discovery.** A canonical manifest at `/api/agent/capabilities` lists every agent-consumable capability with its name, description, -HTTP method and path, required auth scope, response shape, and stable -error codes. The manifest is unauthenticated so an agent without a -token can introspect Pulse before asking for one. +HTTP method and path, required auth scope, response shape, stable +error codes, and the deduplicated `requiredScopes` summary for the +current full surface. It also carries the Pulse Intelligence Core, +Patrol, Assistant, and MCP surface contract, including which +affordances each supported operator surface exposes. The manifest is +unauthenticated so an agent without a token can introspect Pulse before +asking for one. **Depth.** `/api/agent/resource-context/{id}` returns the situated picture of one resource in a single read: identity, operator-set @@ -56,49 +60,61 @@ polling the audit endpoint. ## What ships consuming it -Two reference consumers, both standard-library-only, both -manifest-driven: +Three first-party consumers are built on the same manifest: + +- **Settings -> API Access -> Agent integrations** is the in-app + operator surface. It fetches `/api/agent/capabilities` from the + running instance, lists the declared capabilities by category, shows + the manifest-owned surface contract and affordance badges, shows each + capability's method, path, scope, and stable error codes, and + generates client-ready `pulse-mcp` config snippets from the + manifest-owned MCP adapter setup contract: server name, command, + base URL flag, token environment variable, and the supported client config families. + The panel fills in the current Pulse URL for OpenCode's native `opencode.json` / `mcp` shape + and the common `mcpServers` shape for Claude-style clients. + Tokens are still minted in API Access, so the same settings tab covers + "what agents can do" and "which token unlocks it." - **`cmd/agent-probe`** is a small Go binary that walks the discovery, triage, depth, push flow against a running Pulse instance. Useful as a smoke test or worked example for someone building their own integration. -- **`cmd/pulse-mcp`** is the MCP server adapter. Wire it into Claude - Desktop or Claude Code per the README at `cmd/pulse-mcp/README.md` - and Pulse's tools appear natively. The adapter projects each - manifest capability into one MCP tool with auto-derived input - schema; adding capabilities to Pulse extends the MCP surface - without changes in the adapter. Run with `--emit-notifications` - to also translate Pulse's SSE events (`finding.created`, +- **`cmd/pulse-mcp`** is the MCP server adapter. Wire it into any + MCP client that can launch a local server; the README at + `cmd/pulse-mcp/README.md` includes generated setup plus OpenCode, + Claude Desktop, and Claude Code examples from the same adapter contract, + and the in-app Agent integrations panel shows both + OpenCode's native `opencode.json` shape and the common `mcpServers` + block. The adapter projects each manifest capability into one MCP + tool with auto-derived input schema; adding capabilities to Pulse + extends the MCP surface without changes in the adapter. Run with + `--emit-notifications` to also translate Pulse's SSE events (`finding.created`, `approval.pending`, `action.completed`) into JSON-RPC notifications on the stdio channel so autonomous MCP-bound agents can react to push events without holding a separate HTTP connection. +`pulse-mcp` also has a published distribution path: the one-line +installers (`install-mcp.sh` and `install-mcp.ps1`) download the +matching binary from the latest Pulse GitHub Release and verify the +release checksum. Building from source remains available for local +development. + ## What it does not do yet -- Real-world consumer feedback. The substrate ships with two - reference adapters (HTTP and MCP) and end-to-end contract tests, - but no external integration has been load-bearing on it yet. - Until somebody wires `pulse-mcp` into Claude Desktop or builds - against the agent surface from a custom client, the next - meaningful work item is whatever friction usage surfaces, not - more substrate plumbing. +- Real-world consumer feedback. The substrate ships with the in-app + Agent integrations panel, two reference adapters (HTTP and MCP), + release installers, and end-to-end contract tests, but no external + integration has been load-bearing on it yet. The next meaningful + work item is whatever friction first usage surfaces, not more + substrate plumbing. -- A user-facing surface inside Pulse for managing agent - integrations. Today the agent surface is invisible to the - operator running Pulse: there is no Settings panel listing the - declared capabilities, no "generate MCP config snippet" button, - no token template that says "create token for agent - integration." Adding those is what makes the substrate real for - humans, not just for agents. - -- A distribution path for `pulse-mcp`. Today an integrator must - clone the repo and `go build`. A release artifact (Homebrew - formula, Docker image, signed binary) would turn integration - from "build from source" into "install Pulse, copy this - config." +- macOS notarization and package-manager polish. The installer + verifies release checksums, but the first launch of the unsigned + macOS binary can still show a Gatekeeper warning. Homebrew or other + package-manager distribution can sit on top of the release binary + path when usage signal warrants the maintenance. ## Provable claims @@ -133,7 +149,11 @@ manifest-driven: agent-surface paragraphs in the `## Current State` section. - Implementation: `internal/api/agent_*.go` and `internal/api/resources_operator_state.go`. +- In-app setup surface: + `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx`. - MCP adapter: `cmd/pulse-mcp/` (with README). +- MCP installers: `scripts/install-mcp.sh` and + `scripts/install-mcp.ps1`. - HTTP worked example: `cmd/agent-probe/`. - Subsystem dependencies: relevant paragraphs in `agent-lifecycle.md`, `performance-and-scalability.md`, and `storage-recovery.md` under diff --git a/docs/AI.md b/docs/AI.md index 733da8580..637d21cb2 100644 --- a/docs/AI.md +++ b/docs/AI.md @@ -1,17 +1,22 @@ -# Pulse AI +# Pulse Intelligence -Pulse Patrol is available to everyone on the Community plan with BYOK (your own AI provider). Pro adds alert-triggered root-cause analysis and safe remediation workflows, while hosted Cloud carries those capabilities for hosted environments. Learn more at or see [PULSE_PRO.md](PULSE_PRO.md). +Pulse Patrol is available to everyone on the Community plan with BYOK (your own AI provider). Pro adds hands-on Patrol modes, issue investigation, governed fixes, verified outcomes, and 90-day history, while hosted Cloud carries those capabilities for hosted environments. Learn more at or see [PULSE_PRO.md](PULSE_PRO.md). --- ## Overview -Pulse includes two model-powered systems: + +Pulse Intelligence is built around a shared **Pulse Intelligence Core**: Canonical context, governed actions, safety gates, approval state, action audit, and verification shared by Pulse Assistant, Pulse MCP, and Pulse Patrol. -1. **Pulse Assistant** — An interactive chat interface for ad-hoc troubleshooting, investigations, and infrastructure control. -2. **Pulse Patrol** — A scheduled, context-aware model workflow that gathers infrastructure evidence, exposes governed tools, and asks your configured LLM to report actionable findings. +That core is deliberately surfaced with Patrol as the primary built-in operator and Assistant plus MCP as access paths over the same governed capabilities: -Both systems are built on the same tool-driven architecture: the configured LLM owns diagnosis, prioritization, remediation reasoning, and tool choice; Pulse supplies context, tools, safety gates, approval state, and audit trails. +1. **Pulse Patrol**: Patrol is the first-party operations surface: it watches, investigates, acts within the chosen Patrol mode, verifies outcomes, and records what happened. +2. **Pulse Assistant**: The contextual explanation, approval, and handoff surface for Patrol findings, governed actions, verification, and operator questions. Affordances: tools and interactive questions. +3. **Pulse MCP**: The external-agent adapter that projects canonical Pulse Intelligence capabilities as MCP tools. Affordances: tools, resources, prompts, and capability metadata. + + +These surfaces are built on the same action-driven architecture: the configured LLM owns diagnosis, prioritization, fix reasoning, and action choice; Pulse supplies context, capabilities, safety gates, approval state, and audit trails. Verification is part of the governed action lifecycle rather than a separate model-owned feature. ### Not Just Another Chatbot @@ -21,14 +26,14 @@ Pulse Assistant is a **protocol-driven, safety-gated LLM tool surface** that: - **Caches session facts** — extracts bounded tool facts to avoid redundant queries during the current conversation - **Enforces workflow invariants** — FSM prevents dangerous state transitions - **Supports parallel tool execution** — efficient batch operations with concurrency control -- **Detects and prevents hallucinations** — phantom execution detection +- **Grounds answers in real tool work** — visible tool traces, read-after-write verification, and transcript hygiene prevent unsupported execution claims from being treated as facts - **Returns structured tool errors** — the model can recover from clear, machine-readable failures 📖 **For a deep technical dive into the Assistant architecture, see [architecture/pulse-assistant-deep-dive.md](architecture/pulse-assistant-deep-dive.md).** ### Not Just Another Alerting System -Pulse Patrol is a **scheduled model-owned operations loop** that: +Pulse Patrol is a **scheduled and event-triggered governed operator** that: - **Assembles evidence** from metrics, storage, backups, discovery, alerts, and resource timelines - **Provides statistical context** such as baselines, trend summaries, capacity estimates, and event relationships @@ -42,6 +47,19 @@ All while running entirely on your infrastructure with BYOK for complete privacy See [architecture/pulse-assistant.md](architecture/pulse-assistant.md) for the original safety architecture documentation. +### Assistant And MCP + +Pulse Assistant and `pulse-mcp` are sibling surfaces over Pulse Intelligence, +not competing implementations, and neither replaces the other. Assistant remains +the in-app Pro surface for current resource/finding/run handoffs, approval +cards, governed action status, and operator-friendly timelines. `pulse-mcp` +owns the external-agent bridge: it fetches `/api/agent/capabilities`, projects +those canonical API capabilities as MCP tools, and preserves the same stable +error envelopes and approval/audit contracts. New operational capabilities +should be added to the canonical API manifest first, then consumed by Assistant +or MCP as appropriate; MCP-only actions and Assistant-only copies of the same +business logic are drift. + --- ## Pulse Patrol @@ -71,7 +89,7 @@ patrol_report_finding() / patrol_resolve_finding() ── model-owned finding li model-reported findings ── validated, deduplicated, stored │ ▼ (if configured) -MaybeInvestigateFinding() ── model investigation + governed remediation +MaybeInvestigateFinding() ── model investigation + governed fix planning/execution ``` ### What Patrol Sees @@ -162,23 +180,22 @@ Dismissed and resolved findings persist across Pulse restarts. --- -## Autonomy Levels +## Patrol Modes -Patrol supports three autonomy modes that control how much action it can take: +Patrol supports four modes that decide how far Pulse can go after it finds an issue: | Mode | Behavior | Plan | -|------|----------|------| -| **Monitor** | Detect issues only. No investigation or fixes. | Community (BYOK) | -| **Investigate** | Investigates findings and proposes fixes. All fixes require approval before execution. | Pro / hosted Cloud | -| **Remediate** | Runs governed remediation actions and verifies results. Critical findings still require approval by default. | Pro / hosted Cloud | +|-------|----------|------| +| **Watch only** | Detect issues only. No investigation or fixes. | Community (BYOK) | +| **Ask before changes** | Investigates findings and proposes fixes. All fixes require approval before execution. | Pro / hosted Cloud | +| **Auto-fix safe issues** | Runs warning-level governed fixes automatically and verifies results. Critical findings still require approval by default. | Pro / hosted Cloud | +| **Policy autopilot** | Runs eligible governed fixes automatically and verifies results. Use only in environments where this is acceptable. | Pro / hosted Cloud | -Community and Relay installs can still run scheduled Patrol findings with BYOK. -Investigation, proposed remediation, and fix execution are paid AI-operations -capabilities rather than a core monitoring limit. +Community and Relay installs can still run scheduled Patrol findings with BYOK. Watch only remains the free-first baseline; investigation, proposed fixes, and fix execution are paid AI-operations capabilities rather than a core monitoring limit. ### Investigation Flow -When a finding is created in Investigate or Remediate mode: +When a finding is created in a Pro Patrol mode: ``` Finding created @@ -245,24 +262,24 @@ Pulse Assistant is a **tool-driven** chat interface. It does not "guess" system 2. **Investigate**: Uses `pulse_read` to run bounded, read-only commands and check status/logs 3. **Act** (optional): Uses `pulse_control` for changes, then verifies with a read -### Available Tools +### Tool Inventory -| Tool | Classification | Purpose | -|------|---------------|---------| -| `pulse_query`, `pulse_discovery` | Resolve | Resource discovery and query | -| `pulse_read` | Read | Read-only operations: exec, file, find, tail, logs | -| `pulse_metrics` | Read | Performance metrics and baselines | -| `pulse_storage` | Read | Storage pools, backups, snapshots, Ceph, RAID, disk health | -| `pulse_kubernetes` | Read | Kubernetes cluster info | -| `pulse_pmg` | Read | Proxmox Mail Gateway stats | -| `pulse_alerts` | Read/Write | Alert management (resolve/dismiss are writes) | -| `pulse_docker` | Read/Write | Docker operations (control/update are writes) | -| `pulse_knowledge` | Read/Write | Knowledge persistence (remember/note/save are writes) | -| `pulse_file_edit` | Read/Write | File operations (write/append are writes) | -| `pulse_control` | Write | Guest control, service management | -| `patrol_report_finding` | Patrol | Report a new finding (patrol runs only) | -| `patrol_resolve_finding` | Patrol | Resolve an active finding (patrol runs only) | -| `patrol_get_findings` | Patrol | List active findings (patrol runs only) | +The Assistant tool list is registry-owned at runtime, not hand-maintained in +this public overview. Each turn receives an available-tool manifest generated +from Pulse's governed tool registry, including action mode (`read`, `mixed`, +`write`) and approval policy (`scope_only`, `action_plan`). That same registry +feeds the Assistant system prompt, provider tool declarations, tool-result +handling, approval boundaries, and Patrol-only tool filtering. + +For the current source-owned inventory, see the native tool registry and +governance projection in `internal/ai/tools/` and +`internal/agentcapabilities/`. For external agents, use the live +`/api/agent/capabilities` manifest or `pulse-mcp` `tools/list`; those surfaces +project the canonical agent capabilities rather than a separate MCP-only tool +table. +The same manifest also carries reusable `workflowPrompts` metadata so Pulse +Assistant-compatible starters and MCP `prompts/list` clients discover the same +fleet triage, resource investigation, and Patrol finding review workflows. ### Safety Gates @@ -271,7 +288,7 @@ The assistant enforces multiple safety gates: 1. **Discovery Before Action** — Action tools cannot operate on resources that weren't first discovered 2. **Verification After Write** — After any write, the model must perform a read/status check before providing a final answer 3. **Read/Write Separation** — Read operations route through `pulse_read` (stays in READING state); write operations route through `pulse_control` (enters VERIFYING state) -4. **Phantom Detection** — Detects when the model claims execution without tool calls +4. **Grounded Execution Guardrails** — Visible tool traces and read-after-write checks prevent unsupported execution claims from being treated as facts 5. **Approval Mode** — In Controlled mode, every write requires explicit user approval 6. **Execution Context Binding** — Commands execute within the resolved resource's context, not on parent hosts @@ -297,11 +314,11 @@ When control level is **Controlled**, write actions pause for approval: ## Configuration -Configure in the UI: **Settings → System → AI Assistant** +Configure providers in the UI: **Settings → Pulse Intelligence → Provider & Models** ### Supported Providers -- **Anthropic** (API key or OAuth) +- **Anthropic** (API key) - **OpenAI** - **OpenRouter** - **DeepSeek** @@ -309,6 +326,10 @@ Configure in the UI: **Settings → System → AI Assistant** - **Ollama** (self-hosted, with tool/function calling support) - **OpenAI-compatible base URL** (for providers that implement the OpenAI API shape) +Legacy Anthropic OAuth fields may still appear in stored settings so existing +installs can disconnect and clear old tokens, but Anthropic OAuth is not a +supported runtime authentication method and does not make Anthropic configured. + ### Models Pulse uses model identifiers in the form: `provider:model-name` @@ -316,7 +337,7 @@ Pulse uses model identifiers in the form: `provider:model-name` You can set separate models for: - Chat (`chat_model`) - Patrol (`patrol_model`) -- Safe remediation model (`auto_fix_model`) +- Patrol fix model (`auto_fix_model`, retained as the compatibility settings key) ### Storage @@ -421,10 +442,10 @@ go run ./cmd/eval -scenario resource-context -url http://127.0.0.1:7655 -user ad Pulse includes settings that control how "active" AI features are: -- **Autonomous mode (Pro and above)**: When enabled, AI may execute safe commands without approval -- **Safe remediation workflows (Pro and above)**: Allows Patrol to propose and run governed remediation actions under the approval or autonomy policy you choose -- **Alert-triggered analysis (Pro and above)**: Limits AI to analyzing specific events when alerts occur -- **Full autonomy unlock (Pro and above)**: Permits critical remediation actions without approval (requires explicit toggle) +- **Patrol modes (Pro and above)**: Lets you choose whether Patrol only watches, asks before changes, handles safe fixes, or uses policy autopilot +- **Governed fixes (Pro and above)**: Allows Patrol to propose, approve, run, verify, and record fixes under the Patrol mode you choose +- **Issue investigation (Pro and above)**: Lets Patrol investigate findings with surrounding infrastructure context +- **Policy autopilot unlock (Pro and above)**: Permits eligible critical fixes without per-fix approval after an explicit opt-in If you enable execution features, ensure agent tokens and scopes are appropriately restricted. @@ -440,7 +461,7 @@ Use this only in trusted environments. ## Privacy -Patrol runs on your server and only sends the minimal context needed for analysis to the configured provider (when AI is enabled). Anonymous outbound telemetry (counts and feature flags only — no hostnames or credentials) is enabled by default and can be disabled any time — see [Privacy](PRIVACY.md) for details. +Patrol runs on your server and only sends the minimal context needed for analysis to the configured provider (when AI is enabled). Anonymous outbound telemetry (counts, feature flags, and coarse Patrol mode and governed Pulse Intelligence operations adoption flags and counters only; no hostnames, credentials, prompts, chat messages, command text, action output, token values, or resource identifiers) is enabled by default and can be disabled any time. See [Privacy](PRIVACY.md) for details. --- @@ -449,7 +470,7 @@ Patrol runs on your server and only sends the minimal context needed for analysi Alerts are threshold-based and narrow. Patrol gives the selected model a broader, tool-backed operating picture. - **Alerts**: "Disk > 90%" -- **Patrol**: "The model sees ZFS pool usage, growth rate, datastore consumers, backup context, and governed tools, then decides whether that evidence warrants a finding or action recommendation." +- **Patrol**: "The model sees ZFS pool usage, growth rate, datastore consumers, backup context, and governed actions, then decides whether that evidence warrants a finding or action recommendation." --- @@ -467,7 +488,7 @@ Pulse tracks token usage and costs: | Issue | Solution | |-------|----------| -| AI not responding | Verify provider credentials in **Settings → System → AI Assistant** | +| Assistant or Patrol not responding | Verify provider credentials in **Settings → Pulse Intelligence → Provider & Models** | | No execution capability | Confirm at least one agent is connected | | Findings not persisting | Check Pulse has write access to `ai_findings.json` in the config directory | | Too many findings | This shouldn't happen — please report if it does | @@ -478,7 +499,7 @@ Pulse tracks token usage and costs: ### Deep Dives (Recommended for Technical Audiences) -- **[Pulse Assistant Deep Dive](architecture/pulse-assistant-deep-dive.md)** — Complete technical breakdown of the model-owned tool surface: explicit context, session fact caching, FSM enforcement, parallel execution, phantom detection, structured errors +- **[Pulse Assistant Deep Dive](architecture/pulse-assistant-deep-dive.md)** — Complete technical breakdown of the model-owned tool surface: explicit context, session fact caching, FSM enforcement, parallel execution, grounded execution guardrails, structured errors - **[Pulse Patrol Deep Dive](architecture/pulse-patrol-deep-dive.md)** — Patrol runtime documentation: evidence assembly, deterministic signal extraction, model evaluation, investigation context, investigation orchestration ### Reference Documentation diff --git a/docs/AI_AUTONOMY.md b/docs/AI_AUTONOMY.md index 435a57d84..efb12a771 100644 --- a/docs/AI_AUTONOMY.md +++ b/docs/AI_AUTONOMY.md @@ -1,8 +1,8 @@ -# AI Autonomy and Safety Configuration +# Pulse Intelligence Modes and Safety Configuration -This guide covers how to configure and manage Pulse's AI autonomy levels, control levels, and safety guardrails. +This guide covers how to configure Patrol mode, Pulse Assistant command access, and the safety guardrails that apply before Pulse can change infrastructure. -For a general overview of Pulse AI, see [AI.md](AI.md). For plan-level feature availability, see [PULSE_PRO.md](PULSE_PRO.md). +For a general overview of Pulse Intelligence, see [AI.md](AI.md). For plan-level feature availability, see [PULSE_PRO.md](PULSE_PRO.md). --- @@ -10,39 +10,40 @@ For a general overview of Pulse AI, see [AI.md](AI.md). For plan-level feature a Pulse separates AI permissions into two independent axes: -1. **Patrol Autonomy Level** — How much Patrol can do on its own (detect, investigate, fix). -2. **Assistant Control Level** — Whether the interactive chat assistant can execute commands. +1. **Patrol Mode** — What Patrol may handle automatically after it finds an issue: watch only, ask before changes, handle safe fixes, or use policy autopilot. +2. **Assistant Command Access** — Whether the interactive chat assistant can execute commands during a chat session. -These are configured independently in **Settings → System → AI Assistant**. +Patrol mode is configured on the **Patrol** page. Assistant command access is configured in **Settings → Pulse Intelligence → Assistant**. --- -## Patrol Autonomy Levels +## Patrol Modes -Patrol autonomy controls how aggressively Patrol responds to findings. +Patrol mode sets how far Pulse can go when Patrol finds something that needs attention. -| Level | Key | Detect | Investigate | Fix (Warning) | Fix (Critical) | Plan | -|-------|-----|:------:|:-----------:|:-------------:|:--------------:|------| -| **Monitor** | `monitor` | Yes | No | No | No | Community | -| **Approval** | `approval` | Yes | Yes | Approval required | Approval required | Pro / legacy Pro+ / Cloud | -| **Assisted** | `assisted` | Yes | Yes | Execute automatically | Approval required | Pro / legacy Pro+ / Cloud | -| **Full** | `full` | Yes | Yes | Execute automatically | Execute automatically | Pro / legacy Pro+ / Cloud | +| Mode | Key | Detect | Investigate | Fix warning-level issues | Fix critical issues | Plan | +|-------|-----|:------:|:-----------:|:------------------------:|:-------------------:|------| +| **Watch only** | `monitor` | Yes | No | No | No | Community | +| **Ask before changes** | `approval` | Yes | Yes | Approval required | Approval required | Pro / legacy Pro+ / Cloud | +| **Auto-fix safe issues** | `assisted` | Yes | Yes | Execute automatically | Approval required | Pro / legacy Pro+ / Cloud | +| **Policy autopilot** | `full` | Yes | Yes | Execute automatically | Execute automatically | Pro / legacy Pro+ / Cloud | -- **Monitor** (default): Patrol creates findings but takes no action. This is the Community and Relay baseline. Suitable for learning what Patrol detects before enabling investigation or remediation. -- **Approval** (Pro and above): Patrol investigates findings and proposes fixes. All fixes queue for manual approval before execution. -- **Assisted** (Pro and above): Warning-level safe remediation plans can execute automatically. Critical findings still require approval. This is the recommended starting point for most Pro and legacy Pro+ users who enable fix execution. -- **Full** (Pro and above): Safe remediation plans can execute without approval. Requires an explicit toggle and a Pro, legacy Pro+, or Cloud license. Recommended only for environments with thorough alert coverage. +- **Watch only** (default): Patrol creates findings but takes no action. This is the Community and Relay baseline. Suitable for learning what Patrol detects before enabling investigation or fix execution. +- **Ask before changes** (Pro and above): Patrol investigates findings and proposes fixes. All fixes queue for manual approval before execution. +- **Auto-fix safe issues** (Pro and above): Warning-level safe fix plans can execute automatically. Critical findings still require approval. This is the recommended starting point for most Pro and legacy Pro+ users who enable fix execution. +- **Policy autopilot** (Pro and above): Safe fix plans can execute without approval. Requires an explicit toggle and a Pro, legacy Pro+, or Cloud license. Recommended only for environments with thorough alert coverage. ### Configuration -**UI:** Settings → System → AI Assistant → Patrol Autonomy Level +**UI:** Patrol → Patrol mode **API:** ```bash -# Get current patrol autonomy settings +# Get current Patrol mode settings curl -s -u admin:admin http://localhost:7655/api/ai/patrol/autonomy -# Update autonomy level +# Update Patrol mode. +# The API keeps the autonomy_level field name for compatibility. curl -X PUT http://localhost:7655/api/ai/patrol/autonomy \ -u admin:admin \ -H "Content-Type: application/json" \ @@ -54,7 +55,7 @@ curl -X PUT http://localhost:7655/api/ai/patrol/autonomy \ - `monitor`: Available on all plans. Community and Relay can run Patrol with BYOK. - `approval`, `assisted`, and `full`: Require the `ai_autofix` capability (Pro, legacy Pro+, or Cloud license). -Without the `ai_autofix` capability, the effective autonomy level is clamped to `monitor` at runtime, regardless of the saved configuration. If you previously had a Pro license and downgraded, your saved setting is preserved but enforcement reverts to `monitor`. +Without the `ai_autofix` capability, the effective Patrol mode is clamped to `monitor` at runtime, regardless of the saved configuration. If you previously had a Pro license and downgraded, your saved setting is preserved but enforcement reverts to `monitor`. --- @@ -74,7 +75,7 @@ Control levels govern what the interactive Pulse Assistant can do during chat se ### Configuration -**UI:** Settings → System → AI Assistant → Control Level +**UI:** Settings → Pulse Intelligence → Assistant → Chat command mode **API:** ```bash @@ -100,7 +101,7 @@ Approvals expire after 5 minutes if not acted upon. ## Investigation Configuration -When autonomy is `approval`, `assisted`, or `full`, Patrol investigates findings. These parameters tune investigation behavior: +When Patrol mode is `approval`, `assisted`, or `full`, Patrol investigates findings. These parameters tune investigation behavior: | Setting | Default | Range | Description | |---------|---------|-------|-------------| @@ -115,7 +116,7 @@ When autonomy is `approval`, `assisted`, or `full`, Patrol investigates findings ## Safety Guardrails -Regardless of autonomy level, Pulse enforces multiple safety layers: +Regardless of Patrol mode, Pulse enforces multiple safety layers: ### Blocked Commands @@ -144,12 +145,12 @@ After executing any control action, the assistant must verify the result with a ## Recommended Progression -For new deployments, we recommend gradually increasing autonomy: +For new deployments, gradually increase Patrol mode: -1. **Start with Monitor** — Run Patrol for a few cycles to see what it detects. Dismiss false positives. -2. **Move to Approval where available** — Enable investigation. Review proposed fixes to build confidence. -3. **Use Assisted when fix execution is enabled** — Let Patrol execute warning-level remediation while you approve critical fixes. -4. **Consider Full** — Only if your environment has comprehensive alerting and you trust the fix patterns. +1. **Start with Watch only** — Run Patrol for a few cycles to see what it detects. Dismiss false positives. +2. **Move to Ask before changes where available** — Enable investigation. Review proposed fixes to build confidence. +3. **Use Auto-fix safe issues when fix execution is enabled** — Let Patrol execute warning-level fixes while you approve critical fixes. +4. **Consider Policy autopilot** — Only if your environment has comprehensive alerting and you trust the fix patterns. --- @@ -165,7 +166,7 @@ Prometheus counters (prefix `pulse_patrol_*`) track: ### Cost Tracking Token usage and estimated costs are tracked per provider: -- **UI:** Settings → System → AI Assistant → Usage +- **UI:** Settings → Pulse Intelligence → Provider & Models → Provider Usage & Spend - **API:** `GET /api/ai/cost/summary` - Set monthly budget limits to cap spending @@ -178,7 +179,7 @@ Token usage and estimated costs are tracked per provider: ## Related Documentation -- [Pulse AI Overview](AI.md) — Full AI system documentation +- [Pulse Intelligence Overview](AI.md) — Full Pulse Intelligence system documentation - [Plans and Entitlements](PULSE_PRO.md) — Feature availability by plan - [API Reference](API.md) — Complete API documentation - [Pulse Patrol Deep Dive](architecture/pulse-patrol-deep-dive.md) — Technical architecture details diff --git a/docs/API.md b/docs/API.md index 4145f53b0..4e335ebe2 100644 --- a/docs/API.md +++ b/docs/API.md @@ -940,7 +940,7 @@ Revoke a resource share. Admin or owner role required. --- -## 🤖 Pulse AI +## 🤖 Pulse Intelligence **Paid gating:** endpoints labeled with a paid plan require the relevant Relay, Pro, legacy Pro+, or Cloud capability and return `402 Payment Required` if the feature is not licensed. @@ -960,11 +960,13 @@ Lists models available to the configured providers (queried live from provider A - `POST /api/ai/test` - `POST /api/ai/test/{provider}` -### OAuth (Anthropic) -- `POST /api/ai/oauth/start` (admin) -- `POST /api/ai/oauth/exchange` (admin, manual code input) -- `GET /api/ai/oauth/callback` (public, IdP redirect) -- `POST /api/ai/oauth/disconnect` (admin) +### Legacy Anthropic OAuth Cleanup +Anthropic subscription OAuth is unsupported. These routes remain only for +fail-closed compatibility and token cleanup: +- `POST /api/ai/oauth/start` (admin): returns `501` with `unsupported_anthropic_oauth` +- `POST /api/ai/oauth/exchange` (admin): returns `501` with `unsupported_anthropic_oauth` +- `GET /api/ai/oauth/callback` (public): redirects to settings with `ai_oauth_error=unsupported` unless the provider supplied a specific error +- `POST /api/ai/oauth/disconnect` (admin): clears stored legacy OAuth tokens ### Execute (Chat + Tools) `POST /api/ai/execute` diff --git a/docs/FAQ.md b/docs/FAQ.md index fac1a53c4..60f9396c2 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -32,10 +32,10 @@ If a setting is disabled with an amber warning, it's being overridden by an envi ## 🔍 Monitoring & Metrics ### What do Relay, Pro, and Cloud unlock? -Relay adds secure remote access to the Pulse web UI, Pulse Mobile pairing for handoff, push notifications, and 14-day history. Pro and Cloud unlock **alert-triggered root-cause analysis and safe remediation workflows** along with the broader operations feature set. Existing legacy Pro+ holders keep their current continuity, but self-hosted pricing no longer sells more monitoring volume. Pulse Patrol is available to everyone on Community with BYOK and provides scheduled, cross-system analysis that correlates real-time state, recent metrics history, and diagnostics to surface actionable findings. +Relay adds secure remote access to the Pulse web UI, Pulse Mobile pairing for handoff, push notifications, and 14-day history. Pro and Cloud unlock **hands-on Patrol modes, issue investigation, governed fixes, verified outcomes, and 90-day history** along with the broader operations feature set. Existing legacy Pro+ holders keep their current continuity, but self-hosted pricing no longer sells more monitoring volume. Pulse Patrol is available to everyone on Community with BYOK and provides scheduled, cross-system analysis that correlates real-time state, recent metrics history, and diagnostics to surface actionable findings. Example output includes trend-based capacity warnings, backup regressions, Kubernetes cluster analysis, and correlated container failures that simple threshold alerts miss. -See [Pulse AI](AI.md), [Plans and entitlements](PULSE_PRO.md), and . +See [Pulse Intelligence](AI.md), [Plans and entitlements](PULSE_PRO.md), and . ### Why do VMs show "-" for disk usage? Proxmox API returns `0` for VM disk usage by default. You must install the **QEMU Guest Agent** inside the VM and enable it in Proxmox (VM → Options → QEMU Guest Agent). diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md index 50ecd7aca..31c6b7be3 100644 --- a/docs/PRIVACY.md +++ b/docs/PRIVACY.md @@ -14,9 +14,9 @@ third-party analytics, support diagnostics, or ordinary Settings surfaces. ### Anonymous outbound telemetry -Pulse includes anonymous outbound telemetry that is **enabled by default**. It sends a lightweight ping on startup and once every 24 hours to help me understand how many active installations exist, which releases are actually deployed, and which features are in use. +Pulse includes anonymous outbound telemetry that is **enabled by default**. It sends a lightweight ping on startup and once every 24 hours to help me understand how many active installations exist, which releases are actually deployed, which features are in use, and whether Patrol control and governed Pulse Intelligence operations are being adopted. -No hostnames, credentials, infrastructure identifiers, IP addresses, prompts, chat messages, or personally identifiable information is ever sent. See the full field list below. +No hostnames, credentials, infrastructure identifiers, IP addresses, prompts, chat messages, command text, action output, token values, or personally identifiable information is ever sent. See the full field list below. #### How to disable @@ -80,11 +80,69 @@ Every field is listed below with the reason it exists — nothing else leaves yo | Multi-tenant | `true`/`false` | See whether multi-tenant/runtime-org features are being used | | Paid license | `true`/`false` | Distinguish free from paid posture without sending the exact commercial tier | | Has API tokens | `true`/`false` | See whether token-based automation/integration is being used without sending token counts | +| Pulse Intelligence configured | `true`/`false` | See whether Assistant, Patrol, governed actions, or external-agent access is configured so adoption can be measured without sending configuration details | +| Pulse Intelligence active 30d | `true`/`false` | See whether Assistant, Patrol, external-agent, or governed-action activity occurred in the current 30-day telemetry window | +| Pulse Intelligence governed decision 30d | `true`/`false` | See whether Patrol issue activity reached an approved or rejected governed-action decision without sending prompts, findings, resource identifiers, command text, or action output | +| Pulse Intelligence approved action attempt 30d | `true`/`false` | See whether Patrol issue activity reached an approved governed action attempt without sending action details | +| Pulse Intelligence resolved issue 30d | `true`/`false` | See whether Patrol issue activity reached resolution with an approved action success without sending prompts, findings, resource identifiers, command text, or action output | +| Pulse Intelligence Patrol mode decision 30d | `true`/`false` | See whether a Patrol mode starter, Patrol issue activity, contextual Assistant or external-agent collaboration, and either a rejected governed decision or an approved governed decision with a verified outcome occurred without sending prompt text, checkout details, account links, token details, resource identifiers, command text, or action output | +| Pulse Intelligence Patrol mode resolved issue 30d | `true`/`false` | See whether a Patrol mode starter, Patrol issue activity, contextual Assistant or external-agent collaboration, an approved governed decision, and a verified outcome occurred without sending prompt text, checkout details, account links, token details, resource identifiers, command text, or action output | +| Pulse Intelligence paid Patrol mode decision 30d | `true`/`false` | See whether the install currently has a paid license and Patrol mode reached a governed decision without sending the exact tier, checkout details, account links, prompts, token details, resource identifiers, command text, or action output | +| Pulse Intelligence paid Patrol mode resolved issue 30d | `true`/`false` | See whether the install currently has a paid license and Patrol mode reached a resolved issue without sending the exact tier, checkout details, account links, prompts, token details, resource identifiers, command text, or action output | +| Pulse Intelligence historical Patrol mode decision mirror 30d | `true`/`false` | Compatibility mirror of the Patrol mode decision field for historical aggregate reporting without sending prompt text, checkout details, account links, token details, resource identifiers, command text, or action output | +| Pulse Intelligence historical Patrol mode resolved mirror 30d | `true`/`false` | Compatibility mirror of the Patrol mode resolved field for historical aggregate reporting without sending prompt text, checkout details, account links, token details, resource identifiers, command text, or action output | +| Pulse Intelligence paid historical Patrol mode decision mirror 30d | `true`/`false` | Compatibility mirror of the paid Patrol mode decision field for historical aggregate reporting without sending exact tier, checkout details, account links, token details, resource identifiers, command text, or action output | +| Pulse Intelligence paid historical Patrol mode resolved mirror 30d | `true`/`false` | Compatibility mirror of the paid Patrol mode resolved field for historical aggregate reporting without sending exact tier, checkout details, account links, token details, resource identifiers, command text, or action output | +| Pulse Intelligence governed action active 30d | `true`/`false` | See whether Patrol or Assistant reached action planning, approval, approve/reject decision, or approved-action depth in the current 30-day telemetry window without sending action details | +| Pulse Intelligence Assistant governed decision 30d | `true`/`false` | See whether Assistant collaboration specifically reached the Patrol issue plus approved/rejected governed-action path without sending prompts, chat messages, tool details, findings, command text, or action output | +| Pulse Intelligence Assistant approved action attempt 30d | `true`/`false` | See whether Assistant collaboration specifically reached approved governed-action execution in the 30-day window without sending prompts, chat messages, command text, actors, or action output | +| Pulse Intelligence Assistant approved action success 30d | `true`/`false` | See whether Assistant collaboration specifically reached approved action success in the 30-day window without sending prompts, chat messages, verification details, command text, or action output | +| Pulse Intelligence Assistant resolved issue 30d | `true`/`false` | See whether Assistant collaboration specifically reached Patrol resolution plus approved action success in the 30-day window without sending findings, prompts, chat messages, verification details, command text, or action output | +| Pulse Intelligence external-agent governed decision 30d | `true`/`false` | See whether direct external-agent or MCP collaboration reached the Patrol issue plus approved/rejected governed-action path without sending route parameters, prompts, resource identifiers, command text, or action output | +| Pulse Intelligence external-agent approved action attempt 30d | `true`/`false` | See whether direct external-agent or MCP collaboration reached approved governed-action execution in the 30-day window without sending route parameters, command text, actors, or action output | +| Pulse Intelligence external-agent approved action success 30d | `true`/`false` | See whether direct external-agent or MCP collaboration reached approved action success in the 30-day window without sending route parameters, command text, verification details, or action output | +| Pulse Intelligence external-agent resolved issue 30d | `true`/`false` | See whether direct external-agent or MCP collaboration reached Patrol resolution plus approved action success in the 30-day window without sending findings, route parameters, command text, verification details, or action output | +| Pulse Intelligence `pulse-mcp` governed decision 30d | `true`/`false` | See whether the `pulse-mcp` adapter specifically reached the Patrol issue plus approved/rejected governed-action path without sending prompts, tool inputs, route parameters, resource identifiers, command text, or action output | +| Pulse Intelligence `pulse-mcp` approved action attempt 30d | `true`/`false` | See whether the `pulse-mcp` adapter specifically reached approved governed-action execution in the 30-day window without sending prompts, tool inputs, command text, actors, or action output | +| Pulse Intelligence `pulse-mcp` approved action success 30d | `true`/`false` | See whether the `pulse-mcp` adapter specifically reached approved action success in the 30-day window without sending prompts, tool inputs, verification details, command text, or action output | +| Pulse Intelligence `pulse-mcp` resolved issue 30d | `true`/`false` | See whether the `pulse-mcp` adapter specifically reached Patrol resolution plus approved action success in the 30-day window without sending findings, prompts, tool inputs, verification details, command text, or action output | +| Pulse Intelligence starter requests 30d | `3` | Count shared Patrol-work starter requests in the current 30-day telemetry window without sending prompt text, chat messages, tool inputs, resource IDs, or request details | +| Pulse Intelligence Assistant starter requests 30d | `2` | Count Assistant requests for shared Patrol work in the current 30-day telemetry window without sending prompt text, chat messages, resource IDs, or request details | +| Pulse Intelligence Patrol starter requests 30d | `1` | Count Patrol work starter requests in the current 30-day telemetry window without sending prompt text, findings, resource IDs, or request details | +| Pulse Intelligence Patrol mode starter requests 30d | `1` | Count Patrol work, Patrol-mode, and historical entry-point requests for shared Patrol work in the current 30-day telemetry window without sending prompt text, checkout details, account links, resource IDs, or request details | +| Pulse Intelligence historical Patrol mode starter mirror 30d | `1` | Count historical entry-point requests for Patrol mode in the current 30-day telemetry window without sending prompt text, checkout details, account links, resource IDs, or request details | +| Pulse Intelligence `pulse-mcp` starter requests 30d | `1` | Count `pulse-mcp` requests for shared Patrol work in the current 30-day telemetry window without sending prompt text, tool inputs, resource IDs, route parameters, or request details | +| Pulse Intelligence Assistant AI calls 30d | `18` | Count Assistant model calls in the current 30-day telemetry window without sending prompts, responses, session IDs, or chat text | +| Pulse Intelligence Assistant context AI calls 30d | `7` | Count Assistant model calls tied to a governed resource, finding, handoff, or action context in the current 30-day telemetry window without sending prompts, responses, session IDs, resource IDs, finding IDs, or chat text | +| Pulse Intelligence Assistant tool calls 30d | `11` | Count Assistant tool calls in the current 30-day telemetry window without sending tool names, tool inputs, tool outputs, prompts, responses, session IDs, resource IDs, finding IDs, command text, or chat text | +| Pulse Intelligence Patrol AI calls 30d | `6` | Count Patrol model calls in the current 30-day telemetry window without sending provider-bound context or findings text | +| Pulse Intelligence Patrol runs 30d | `12` | Count Patrol investigations in the current 30-day telemetry window | +| Pulse Intelligence Patrol new findings 30d | `5` | Count new findings produced by Patrol in the current 30-day telemetry window without sending finding IDs or details | +| Pulse Intelligence Patrol investigations 30d | `3` | Count findings investigated by Patrol in the current 30-day telemetry window without sending finding IDs, resource IDs, or details | +| Pulse Intelligence Patrol resolved findings 30d | `2` | Count findings resolved or fix-verified in the current 30-day telemetry window without sending finding IDs, resource IDs, fix details, or verification detail | +| Pulse Intelligence Patrol autofixes 30d | `1` | Count Patrol autofix records in the current 30-day telemetry window without sending target resources or fix content | +| Pulse Intelligence external agent enabled | `true`/`false` | See whether at least one token can use the external Pulse Intelligence agent/MCP surface without sending token counts, names, scopes, or values | +| Pulse Intelligence external agent used 30d | `true`/`false` | See whether an external-agent-capable API token reached a Pulse Intelligence agent/MCP route in the current 30-day telemetry window without sending token identity, route parameters, resource IDs, or request details | +| Pulse Intelligence MCP adapter used 30d | `true`/`false` | See whether the `pulse-mcp` adapter reached a Pulse Intelligence agent/MCP route in the current 30-day telemetry window without sending token identity, route parameters, resource IDs, prompts, or request details | +| Pulse Intelligence external agent context requests 30d | `8` | Count external-agent/MCP resource-context and fleet-context requests in the current 30-day telemetry window without sending resource IDs, route parameters, or request details | +| Pulse Intelligence external agent event stream requests 30d | `3` | Count external-agent/MCP event-stream requests in the current 30-day telemetry window without sending event content, route parameters, or request details | +| Pulse Intelligence external agent provisioning requests 30d | `2` | Count external-agent/MCP provisioning requests in the current 30-day telemetry window without sending discovered resources, credentials, route parameters, or request details | +| Pulse Intelligence external agent operator state requests 30d | `5` | Count external-agent/MCP operator-state requests in the current 30-day telemetry window without sending state payloads, route parameters, or request details | +| Pulse Intelligence external agent finding requests 30d | `4` | Count external-agent/MCP finding-list and finding-decision requests in the current 30-day telemetry window without sending finding IDs, finding text, route parameters, or request details | +| Pulse Intelligence external agent action requests 30d | `1` | Count external-agent/MCP action-plan, action-decision, and action-execution requests in the current 30-day telemetry window without sending command text, action output, route parameters, or request details | +| Pulse Intelligence action plans 30d | `4` | Count governed action plans in the current 30-day telemetry window without sending command text, resource IDs, or plan details | +| Pulse Intelligence approval requests 30d | `2` | Count approval-gated action requests in the current 30-day telemetry window without sending approvers, reasons, command text, or targets | +| Pulse Intelligence rejected action decisions 30d | `1` | Count rejected governed action decisions in the current 30-day telemetry window without sending approvers, reasons, command text, targets, or action IDs | +| Pulse Intelligence approved action decisions 30d | `1` | Count approved governed action decisions in the current 30-day telemetry window without sending approvers, reasons, command text, targets, or action IDs | +| Pulse Intelligence approved action attempts 30d | `1` | Count approved governed action attempts in the current 30-day telemetry window without sending action output, command text, or verification detail | +| Pulse Intelligence approved action successes 30d | `1` | Count approved governed actions that completed successfully in the current 30-day telemetry window without sending action output, command text, resource IDs, actors, reasons, or verification detail | #### Server-side handling and retention - Telemetry pings are stored on the Pulse license server only for aggregate install/use analysis. -- The license server stores only the same coarse telemetry fields listed above; it does not expand them into exact commercial tiers or exact API-token counts. +- The license server stores only the same coarse telemetry fields listed above; it does not expand them into exact commercial tiers, exact API-token counts, prompts, chat messages, command text, action output, token values, or resource identifiers. +- Pulse may derive aggregate Pulse Intelligence adoption reports from those same rows, including whether an install reached Patrol issue activity, Patrol resolution, Assistant, direct external-agent, or MCP collaboration, Patrol mode starter use, paid Patrol mode cohorts, governed-action activity, approved or rejected action decisions, approved action success, recent retention, and observed free-to-paid movement within the source window. Those reports do not add prompts, findings, resource identifiers, tool names, tool inputs, tool outputs, command payloads, action outputs, account links, or exact commercial tiers. +- External-agent/MCP activity is stored only as a coarse adapter-origin flag plus capability-class counters: context, event stream, provisioning, operator state, findings, and action requests. - Telemetry rows older than **90 days** are purged automatically. - The license server uses client IP addresses transiently for abuse/rate limiting, but it does **not** store IP addresses in telemetry rows. @@ -93,7 +151,7 @@ Every field is listed below with the reason it exists — nothing else leaves yo - No IP addresses are stored in telemetry rows - No hostnames, node names, VM names, or any infrastructure identifiers - No Proxmox credentials, API tokens, or passwords -- No alert content, AI prompts, or chat messages +- No alert content, AI prompts, chat messages, tool names, tool inputs, tool outputs, command text, action output, or token values - No personally identifiable information of any kind #### Install ID rotation diff --git a/docs/PULSE_PRO.md b/docs/PULSE_PRO.md index 2c15f612b..8b2413476 100644 --- a/docs/PULSE_PRO.md +++ b/docs/PULSE_PRO.md @@ -91,8 +91,8 @@ capability key is a primary v6 product pillar. These are the current self-hosted Pro pillars that Pulse should keep investing in, surfacing, and marketing: -- Alert-triggered root-cause analysis. -- Safe remediation workflows through Patrol fix execution and autonomy controls. +- Patrol investigates issues. +- Patrol handles safe fixes through approval-backed execution and Patrol mode. - 90-day history. - Included team/admin extras: RBAC, audit logging, reporting, and agent profiles. SSO is included with Community and higher tiers. @@ -110,7 +110,7 @@ be elevated into headline Pro marketing or generic upgrade prompts: These should not appear as current v6 Pro promises unless they are rebuilt into first-class product surfaces: - `incident memory` as a standalone feature name -- `scheduled remediations` +- `scheduled automated fixes` - `execution audit trail` ## Paid Feature Proof Map @@ -124,7 +124,7 @@ surface upgrade prompts unless the user deliberately enters a commercial path. |---|---|---| | Self-hosted monitoring is not sold by monitored-system or child-resource volume. | `pkg/licensing/features.go` and `pkg/licensing/entitlement_payload.go` normalize self-hosted limits to the current no-volume-gate policy. | `pkg/licensing/grant_claims_contract_test.go`, `pkg/licensing/activation_types_test.go`, and `internal/api/licensing_handlers_auto_migrate_test.go` prove self-hosted paid/legacy continuity does not surface finite monitored-system allowances. | | Relay includes secure remote web access, Pulse Mobile pairing for handoff, push notifications, and 14-day history. | `pkg/licensing/features.go` grants `relay`, `mobile_app`, `push_notifications`, and `long_term_metrics` to Relay with `TierHistoryDays[relay] == 14`; relay onboarding/settings routes are gated behind Relay. | `pkg/licensing/features_test.go`, `pkg/licensing/entitlement_payload_test.go`, `internal/api/relay_sso_license_gating_test.go`, and `frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx`. | -| Pro includes alert-triggered root-cause analysis and safe remediation workflows. | `internal/api/ai_handlers.go` gates alert-triggered analysis behind `ai_alerts` and remediation/autonomy behind `ai_autofix`; `internal/ai/service.go` enforces the same capabilities in service-level paths. | `pkg/licensing/features_test.go`, `internal/api/router_routes_ai_execute_stream_test.go`, `internal/api/ai_intelligence_handlers_remediation_more_test.go`, and `frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx`. | +| Pro includes Patrol issue investigation and verified fix actions. | `internal/api/ai_handlers.go` gates alert-triggered analysis behind `ai_alerts` and fix/autonomy behavior behind `ai_autofix`; `internal/ai/service.go` enforces the same capabilities in service-level paths. | `pkg/licensing/features_test.go`, `internal/api/router_routes_ai_execute_stream_test.go`, `internal/api/ai_intelligence_handlers_remediation_more_test.go`, and `frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx`. | | Pro includes 90-day history. | `pkg/licensing/features.go` sets `TierHistoryDays[pro] == 90`; `pkg/licensing/entitlement_payload.go` emits `max_history_days`; `frontend-modern/src/stores/license.ts` and `frontend-modern/src/components/shared/useHistoryChartState.ts` lock ranges above the entitlement. | `pkg/licensing/features_test.go`, `pkg/licensing/entitlement_payload_test.go`, and `frontend-modern/src/stores/__tests__/license.test.ts`. | | Pro includes business/admin extras: RBAC, audit logging, reporting, and agent profiles. | Router and settings gates use `rbac`, `audit_logging`, `advanced_reporting`, and `agent_profiles`; audit capture is SQLite-backed in `pkg/server/server.go` and `pkg/audit/sqlite_factory.go`, while query/export remains license-gated. | `internal/api/security_regression_test.go`, `internal/api/rbac_lifecycle_test.go`, `pkg/reporting/catalog_test.go`, and `frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx`. | @@ -140,8 +140,8 @@ This matrix is derived from the canonical table in `docs/architecture/ENTITLEMEN |---|---|---|:---:|:---:|:---:|:---:|---| | `FeatureAIPatrol` | `ai_patrol` | Pulse Patrol (Background Health Checks) | Y | Y | Y | Y | Patrol itself is available on Community with your own provider or local model. Higher-autonomy outcomes and fix execution are separately gated. | | `FeatureRelay` | `relay` | Remote Access (Mobile Relay) | N | Y | Y | Y | API route gating via `RequireLicenseFeature(..., relay, ...)` for relay settings and onboarding endpoints. | -| `FeatureAIAlerts` | `ai_alerts` | Alert-Triggered Root-Cause Analysis | N | N | Y | Y | API route gating via `RequireLicenseFeature(..., ai_alerts, ...)`. | -| `FeatureAIAutoFix` | `ai_autofix` | Safe Remediation Workflows | N | N | Y | Y | Required for governed fix execution and autonomous remediation actions. | +| `FeatureAIAlerts` | `ai_alerts` | Patrol Investigates Issues | N | N | Y | Y | API route gating via `RequireLicenseFeature(..., ai_alerts, ...)`. | +| `FeatureAIAutoFix` | `ai_autofix` | Patrol Handles Safe Fixes | N | N | Y | Y | Required for governed fix execution and automatic Patrol actions. | | `FeatureKubernetesAI` | `kubernetes_ai` | Kubernetes AI Analysis (Compatibility) | N | N | Y | Y | Legacy compatibility gate for `/api/ai/kubernetes/analyze`; not a primary marketed v6 Pro plan pillar. | | `FeatureAgentProfiles` | `agent_profiles` | Centralized Agent Profiles | N | N | Y | Y | API route gating via `RequireLicenseFeature(..., agent_profiles, ...)`. | | `FeatureUpdateAlerts` | `update_alerts` | Update Alerts (Container/Package Updates) | Y | Y | Y | Y | Included in Community tier per `TierFeatures[TierFree]`. | @@ -156,16 +156,16 @@ This matrix is derived from the canonical table in `docs/architecture/ENTITLEMEN | `FeatureUnlimited` | `unlimited` | Hosted Capacity Policy | N | N | N | Y | Hosted/enterprise capacity policy only; not a self-hosted core monitoring gate. | | `FeatureWhiteLabel` | `white_label` | White-Label Report Branding | N | N | N | Y* | Gates custom report branding. Provider defaults and per-client overrides render only when this entitlement is active. | -## Autonomy Levels (AI Safety) +## Patrol Modes -Patrol and the Assistant support tiered autonomy: +Patrol mode decides how far Pulse can go after Patrol finds something that needs attention. Assistant chat command access is configured separately. | Mode | Behavior | Plan | |---|---|---| -| **Monitor** | Detect issues only. No investigation or remediation execution. | Community / Relay | -| **Investigate** | Investigates findings and proposes fixes. All remediation actions require approval. | Pro / hosted Cloud | -| **Remediate** | Runs approved safe remediation actions and verifies the outcome. Critical findings require approval by default. | Pro / hosted Cloud | -| **Full autonomy** | Allows critical remediation actions without approval when explicitly enabled. | Pro / hosted Cloud | +| **Watch only** | Detect issues only. No investigation or fix execution. | Community / Relay | +| **Ask before changes** | Investigates findings and proposes fixes. All fixes require approval before execution. | Pro / hosted Cloud | +| **Auto-fix safe issues** | Runs approved safe fixes and verifies the outcome. Critical findings require approval by default. | Pro / hosted Cloud | +| **Policy autopilot** | Runs eligible policy-approved fixes without approval when explicitly enabled. | Pro / hosted Cloud | ## What You Get (By Plan) @@ -183,8 +183,8 @@ Patrol and the Assistant support tiered autonomy: ### Pro - Everything in Relay, plus: -- Alert-triggered root-cause analysis. -- Safe remediation workflows and autonomy controls. +- Patrol investigates issues. +- Patrol handles safe fixes through Patrol mode. - Centralized agent profiles. - RBAC, audit logging, and advanced reporting. - 90-day history. @@ -223,4 +223,4 @@ This returns a feature map including keys like `relay`, `ai_alerts`, `ai_autofix - [Pulse Patrol Deep Dive](architecture/pulse-patrol-deep-dive.md) - [Pulse Assistant Deep Dive](architecture/pulse-assistant-deep-dive.md) -- [Pulse AI overview](AI.md) +- [Pulse Intelligence overview](AI.md) diff --git a/docs/README.md b/docs/README.md index b8b0bc504..7e95c2228 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,7 +65,7 @@ All other documents are supporting references unless explicitly required for evi ## 📖 Advanced Topics (Relay / Pro / legacy Pro+ / Cloud) -- **[AI Autonomy & Safety](AI_AUTONOMY.md)** – Configure patrol autonomy levels, assistant control levels, investigation tuning, and safety guardrails. +- **[AI Modes & Safety](AI_AUTONOMY.md)** – Configure Patrol mode, assistant control levels, investigation tuning, and safety guardrails. - **[Role-Based Access Control (RBAC)](RBAC.md)** – Define custom roles, assign permissions, and integrate with OIDC group mapping. - **[Audit Logging](AUDIT_LOGGING.md)** – Tamper-evident event logging for compliance, with query, export, and signature verification. @@ -77,7 +77,7 @@ All other documents are supporting references unless explicitly required for evi - **[Relay / Pulse Mobile Handoff](RELAY.md)** – End-to-end encrypted relay for supported Pulse Mobile clients (Relay and above). - **[Recovery Central](RECOVERY.md)** – Unified backup, snapshot, and replication view across all providers. - **[Pulse Cloud (Hosted)](CLOUD.md)** – Fully managed hosting with automatic updates and backups. -- **[Pulse AI](AI.md)** – Chat assistant, patrol findings, alert analysis, intelligence, and forecasts. +- **[Pulse Intelligence](AI.md)** – Pulse Assistant, Patrol findings, alert analysis, governed actions, and forecasts. - **[Metrics History](METRICS_HISTORY.md)** – Persistent metrics storage with configurable retention. - **[Mail Gateway](MAIL_GATEWAY.md)** – Proxmox Mail Gateway (PMG) monitoring. - **[Auto Updates](AUTO_UPDATE.md)** – One-click updates for supported deployments. @@ -91,7 +91,7 @@ Pulse is available in three self-hosted tiers plus hosted Cloud: - **Community**: Free self-hosted monitoring with core monitoring included and 7-day history. - **Relay**: Adds secure remote access to the Pulse web UI, Pulse Mobile pairing for handoff, push notifications, and 14-day history. -- **Pro**: Adds alert-triggered root-cause analysis, safe remediation workflows, operations tooling, governance features, and 90-day history. +- **Pro**: Adds hands-on Patrol modes, issue investigation, governed fixes, verified outcomes, operations tooling, governance features, and 90-day history. - **Cloud**: Hosted Pulse with Pro-level capabilities; hosted pricing is unchanged by the self-hosted model lock. - **[Learn more at pulserelay.pro](https://pulserelay.pro)** diff --git a/docs/architecture/v6-pricing-and-tiering.md b/docs/architecture/v6-pricing-and-tiering.md index 4fe48f920..f536f43e9 100644 --- a/docs/architecture/v6-pricing-and-tiering.md +++ b/docs/architecture/v6-pricing-and-tiering.md @@ -106,9 +106,9 @@ than sold by monitored-system volume. | SSO | OIDC/SAML with multi-provider support | | Update notifications | Yes | | Metrics history | **7 days** | -| AI Patrol | Monitor + root-cause summaries + fix suggestions with the operator's configured provider or local model | -| Safe remediation workflows | **No** (must apply fixes manually) | -| Alert-triggered root-cause analysis | No | +| AI Patrol | Watch-only findings with the operator's configured provider or local model | +| Patrol governed fixes | **No** (must apply fixes manually) | +| Alert investigation | No | | Relay | No | | Push notifications | No | | Customer-specific Relay URL | No | @@ -123,9 +123,10 @@ than sold by monitored-system volume. it's their provider, their key or local runtime, and their operating cost. Pulse applies technical guardrails such as anti-loop protection and rate protection. -In both modes, the paid gate is governed execution through Pulse: free users see the -full analysis and fix suggestions but apply fixes manually. Pro users can run approved -remediation actions through Pulse. +In both modes, the paid gate is governed execution through Pulse: free users see +watch-only Patrol findings but apply fixes manually. Pro users can let Patrol +investigate, ask for approval when policy requires it, run governed fixes, verify +outcomes, and record what happened. ### Relay — $4.99/month or $39/year @@ -138,8 +139,8 @@ remediation actions through Pulse. | Push notifications | **Yes** | | Customer-specific Relay URL | No; Relay uses the standard outbound relay service for v6 GA | | Metrics history | **14 days** | -| Safe remediation workflows | No | -| Alert-triggered root-cause analysis | No | +| Patrol governed fixes | No | +| Alert investigation | No | | RBAC/Audit/Reporting | No | | Reporting | No | @@ -153,8 +154,9 @@ without changing their self-hosted monitoring scope. |---|---| | Monitoring scope | **Core self-hosted monitoring included** | | Everything in Relay | Yes | -| Safe remediation workflows | **Yes** (approved execution, safety preflight, rollback) | -| Alert-triggered root-cause analysis | **Yes** | +| Patrol modes | **Yes** (choose how much Patrol can do) | +| Issue investigation | **Yes** | +| Governed fixes | **Yes** (approved execution, safety preflight, rollback, verification) | | Metrics history | **90 days** | | RBAC | **Yes** | | Audit logging | **Yes** | @@ -165,8 +167,8 @@ without changing their self-hosted monitoring scope. **Positioning:** For serious self-hosted operators who want Pulse to move from monitoring into operations. The marketing pitch focuses on three things: -1. "Explain what broke" (alert-triggered root-cause analysis) -2. "Fix it safely" (safe remediation workflows) +1. "Choose Patrol mode" (how much Patrol can do) +2. "Let Patrol investigate and fix governed issues" (issue investigation, governed fixes, verification) 3. "Keep longer operating memory" (90-day history) Relay convenience and the team extras (RBAC, audit logging, reporting, and agent @@ -291,8 +293,8 @@ Patrol finding is a conversion moment. ### 3. Provider setup stays self-managed AI setup on self-hosted installs should point operators at their own provider connection or -local model endpoint. Paid operations features such as safe remediation -workflows, alert analysis, and longer history remain opt-in extras; they do not +local model endpoint. Paid operations features such as governed fixes, +issue investigation, and longer history remain opt-in extras; they do not replace the self-managed AI runtime path. ### 4. Relay as optional convenience ($39/yr) @@ -309,7 +311,7 @@ opens pricing/activation/recovery/support flows, when the session is hosted/clou existing entitlement requires a clear renewal or recovery path. High-intent product moments should stay useful without becoming sales surfaces: -- Patrol finds a fixable issue -> show the manual fix and BYOK/provider path first; safe remediation execution can +- Patrol finds a fixable issue -> show the manual fix and BYOK/provider path first; governed fix execution can remain an opt-in Pro capability where commercial prompts are explicitly allowed. - An alert needs deeper explanation -> keep free investigation context useful; paid alert analysis stays an optional, discoverable extra. @@ -322,7 +324,7 @@ High-intent product moments should stay useful without becoming sales surfaces: Self-hosted Community users should not need a temporary monitored-system overflow path because core self-hosted monitoring is included by default and not metered. Onboarding can still surface Relay and Pro value when users try remote access, push notifications, longer history, alert -investigation, or safe remediation workflows. +investigation, or governed fixes. ### 7. Transparent monitored-system ledger The ledger remains important for inventory truth, hosted/MSP limits, and support. It should @@ -428,7 +430,7 @@ explain monitored-system identity: ### Boundary-upgrade copy - **Community → Relay:** Want Pulse outside your LAN? Upgrade to Relay for secure remote access, mobile, push notifications, and 14-day history. -- **Relay → Pro:** Want Pulse to do more than alert? Upgrade to Pro for root-cause analysis, safe remediation workflows, and 90-day history. +- **Relay → Pro:** Want Pulse to do more than alert? Upgrade to Pro for hands-on Patrol modes, issue investigation, governed fixes, verified outcomes, and 90-day history. - **Existing self-hosted monitoring:** Your monitored systems keep working. Paid self-hosted tiers add convenience and operations features, not more monitoring room. --- diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index 2958aab7d..46179c90c 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -2750,8 +2750,8 @@ "src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts", "src/components/patrol/__tests__/ApprovalSection.test.tsx", "src/components/AI/__tests__/FindingsPanel.test.ts", - "src/features/patrol/__tests__/PatrolIntelligenceSummary.test.tsx", "src/pages/__tests__/AIIntelligence.test.tsx", + "src/utils/__tests__/patrolEmptyStatePresentation.test.ts", "src/utils/__tests__/patrolPagePresentation.test.ts", "src/utils/__tests__/patrolSummaryPresentation.test.ts", "src/utils/__tests__/aiChatPresentation.test.ts", @@ -2856,11 +2856,6 @@ "path": "frontend-modern/src/components/patrol/__tests__/ApprovalSection.test.tsx", "kind": "file" }, - { - "repo": "pulse", - "path": "frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceSummary.test.tsx", - "kind": "file" - }, { "repo": "pulse", "path": "frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts", @@ -2876,6 +2871,11 @@ "path": "frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx", "kind": "file" }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/patrolEmptyStatePresentation.test.ts", + "kind": "file" + }, { "repo": "pulse", "path": "frontend-modern/src/utils/__tests__/patrolPagePresentation.test.ts", @@ -3911,11 +3911,15 @@ "status": "partial", "completion": { "state": "bounded-residual", - "summary": "Commercial readiness is coherent for explicit preview, checkout handoff, paid-user activation/recovery, and hosted-mode paths, while default self-hosted app surfaces stay free-first and non-promotional; because v6 is not publicly GA, production public checkout stays on v5 until explicit GA cutover approval, and the production public flip plus release-day rollout execution remain a separate promotion-time follow-up.", + "summary": "Commercial readiness is coherent for explicit preview, checkout handoff, paid-user activation/recovery, hosted-mode paths, and the first Pulse Intelligence value-evidence reporting surface, while default self-hosted app surfaces stay free-first and non-promotional; because v6 is not publicly GA, production public checkout stays on v5 until explicit GA cutover approval, and the production public flip plus release-day rollout execution plus the broader governed Patrol control experience remain separate follow-ups.", "tracking": [ { "kind": "lane-followup", "id": "commercial-ga-promotion-package" + }, + { + "kind": "lane-followup", + "id": "pulse-intelligence-pro-activation-loop" } ] }, @@ -6974,6 +6978,21 @@ "L20" ], "subsystem_ids": [] + }, + { + "id": "pulse-intelligence-pro-activation-loop", + "summary": "Track Pulse Intelligence as the default governed Pro operations experience beyond the current feature floor: a new Pro user should choose what Patrol may handle automatically, then Patrol should watch the infrastructure, investigate real issues, act automatically or ask for approval according to that policy, verify outcomes, and record what happened. Assistant and Pulse MCP must expose the same governed capabilities as contextual and external-agent access paths, but they are not the primary first-party success condition. Pro should record whether completing this Patrol control loop improves activation, retention, and paid conversion. This is L2-owned commercial activation work with explicit dependencies on the L6 Assistant architecture, L20 action governance, and L23 Pulse Intelligence floor.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-06-20", + "lane_ids": [ + "L2" + ], + "repo_ids": [ + "pulse", + "pulse-pro" + ], + "subsystem_ids": [] } ], "coverage_gaps": [ @@ -7720,7 +7739,7 @@ }, { "id": "patrol-product-language-ai-boundary", - "summary": "Patrol product-language migration boundary audited: customer-facing page titles, nav, route chrome, summary copy, actions, and empty states lead with Patrol/Pulse Patrol/Pulse Assistant. Internal file, store, type, transport, and log identifiers retain the shared AI-runtime boundary. The `/settings/system-ai` panel is the explicit provider/configuration carve-out where AI terminology is reserved; its chrome already leads with 'Assistant & Patrol'. The remaining AgentProfilesPanel 'Ideas' tooltip was product-neutralized to 'Suggest profiles for your estate'.", + "summary": "Patrol product-language migration boundary audited: customer-facing page titles, nav, route chrome, summary copy, actions, and empty states lead with Patrol/Pulse Patrol/Pulse Assistant. Internal file, store, type, transport, and log identifiers retain the shared AI-runtime boundary. The `/settings/system-ai` route remains the explicit provider/configuration carve-out; its visible chrome now lives under Pulse Intelligence > Provider & Models rather than generic AI settings. The remaining AgentProfilesPanel 'Ideas' tooltip was product-neutralized to 'Suggest profiles for your estate'.", "kind": "architecture", "decided_at": "2026-04-17", "subsystem_ids": [ diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index ad100de95..f635530fa 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -111,6 +111,29 @@ shared `/api/resources` payload carries a backend-authored Proxmox workload target linked to that node agent. Suppressing an inside-guest install cue for that case means governed actions have a node-agent path; it must not be presented as proof that a guest-local agent is installed. +Agent-facing operations-loop status wiring in `internal/api/router.go` and +`internal/api/agent_resource_context.go` is lifecycle-adjacent only because it +shares agent route infrastructure. Workflow starter counts on that endpoint, +contextual Assistant/external-agent collaboration counts inside the Assistant +step, the content-free Patrol control starter split, and Patrol control +completed-loop, resolved-loop, or `patrolControlValueState` proof mirrored to +`patrolAutonomyValueState` compatibility fields are +API-contract telemetry/orientation fields, not agent enrollment, liveness, +setup completion, install, update, command, recovery, or fleet-control +lifecycle authority. Native `pulse_patrol` starts, current paid +`patrol_control` starts, legacy `patrol_autonomy` starts, and legacy +`pulse_pro_activation` starts all contribute to Patrol control starter orientation; +`proActivationOperationsLoopStarterCount` remains a legacy entry-point counter +for the old alias only, while the legacy completed/resolved/value +`proActivation*` fields are compatibility aliases for that same first-party +Patrol control proof while older commercial and telemetry consumers migrate. +Lifecycle surfaces must +not treat `verified`, +legacy-compatible `verified_needs_mcp`, or `governed_decision_recorded` as +proof that an agent install, update, registration, profile rollout, or fleet +command completed. External-agent/MCP readiness is optional collaboration +setup for agents outside the app, not a first-party Patrol control completion +gate and not a lifecycle setup stage. 1. `frontend-modern/src/api/agentProfiles.ts` shared with `api-contracts`: the agent profiles frontend client is both an agent lifecycle control surface and a canonical API payload contract boundary. 2. `frontend-modern/src/api/nodes.ts` shared with `api-contracts`: the shared Proxmox node client is both an agent lifecycle setup/install control surface and a canonical API payload contract boundary. @@ -260,6 +283,11 @@ same browser-safe history boundary. Lifecycle surfaces, MCP adapters, and agents may display the updated title as human navigation metadata, but they must not treat a renamed title as command authorization, host identity, enrollment state, capability evidence, or install-token disclosure. +The native Assistant surface-tool inventory at +`GET /api/ai/assistant/surface-tools` is also AI-runtime/API-contract metadata: +lifecycle surfaces may display which Assistant tools are available, but must +not reinterpret that list as agent enrollment state, command authority, install +token scope, or proof that a host can execute lifecycle actions. Agent lifecycle consumers of `/api/agent/events` and `/api/agent/resource-context/{id}` must also honor the shared API command payload boundary: API tokens with monitoring/read scope receive @@ -268,6 +296,20 @@ text unless they also hold action execution scope. Lifecycle UI and agents may use those redacted events as doorbells or status summaries, but they must fetch governed detail through the approval/action surfaces and must not treat a monitoring-readable event stream as command disclosure or execution authority. +Lifecycle surfaces that inspect agent event kinds must consume the shared +`internal/agentcapabilities` event vocabulary. Local lifecycle, MCP, or probe +copies of `finding.created`, `approval.pending`, `action.completed`, +`stream.connected`, or `heartbeat`, or local SSE parser semantics for those +events, are contract drift because event naming and stream framing are +API/AI-owned, not lifecycle-owned. +Lifecycle consumers of agent-surface failures must also consume the shared +`internal/agentcapabilities` error envelope; lifecycle work may branch on the +stable `error` code but must not define a local failure envelope or reinterpret +agent errors as install, enrollment, command, or update authority. Branchable +agent-surface codes exposed through shared `internal/api/` routes must be +referenced from `agentcapabilities.AgentErrCode*` constants so lifecycle UI, +Assistant handoffs, MCP adapters, and manifest declarations do not drift into +separate string vocabularies. The resource-context endpoint's additive context sections are the same read-only boundary: lifecycle consumers, MCP adapters, and external agents may use their bounded facts, provenance, freshness, and explicit redaction metadata @@ -566,6 +608,40 @@ instead of lifecycle-local centered icon/text shells. lifecycle surfaces may display contact email when supplied by the shared auth boundary, but they must not reinterpret SSO or Stripe email as the canonical user identifier for setup, install, or fleet-management actions. + Approved-action tool invocation parsing in `internal/api/router_routes_ai_relay.go` + is not an agent-lifecycle route grammar: lifecycle-adjacent setup or repair + flows that execute governed Pulse tools must inherit + `internal/agentcapabilities.ParseTextToolInvocation` through the API relay + instead of duplicating `pulse_*` command-string parsing in lifecycle helpers. + Approved-action replay through that same relay must also use the shared + `internal/agentcapabilities` approved-action argument helper, not lifecycle-local + approval argument keys or pass-through maps. + Native Assistant workflow-prompt rendering through + `POST /api/ai/workflow-prompts/render` is likewise AI-runtime/API-contract + ownership even though it is wired through shared `internal/api/` files: + lifecycle surfaces may benefit from the resulting Assistant prompt text, but + they must not treat manifest `workflowPrompts`, rendered prompt bodies, or + Assistant starter availability as install, enrollment, or fleet-control + route grammar. + The adjacent workflow-starter activity markers on + `POST /api/ai/workflow-prompts/activity` and + `POST /api/agent/workflow-prompt-activity` follow that same split: + lifecycle surfaces may use the aggregate activation reports, but the + content-free prompt-name/surface/timestamp marker is not agent enrollment, + install readiness, fleet-control liveness, command authority, or agent + update state. The first-party `pulse_patrol` starter surface means a user + entered the Pulse Intelligence operations-loop journey from app UX; it does + not imply a host agent is installed, healthy, or authorized to run commands. + The first-party `patrol_control` starter surface and legacy + `patrol_autonomy` / `pulse_pro_activation` aliases are paid Patrol control + journey markers only; lifecycle code must not treat any of them as setup, + install, fleet-health, or command-agent state. + The adjacent `GET /api/agent/patrol-control/status` projection in + `internal/api/agent_resource_context.go` may expose operator-readable Patrol + status copy for native and external-agent orchestration, but lifecycle + surfaces must not reinterpret that copy, its next action, or its + completed/resolved Patrol outcome counts as install readiness, enrollment + health, command authorization, or agent update state. Hosted Pulse Account may deep-link an MSP operator into a tenant workspace's agent-install surface through a signed local handoff target such as `/settings/infrastructure?add=linux-host`, but install-token generation, @@ -599,7 +675,20 @@ instead of lifecycle-local centered icon/text shells. Patrol readiness and settings-save payload changes on those shared handlers are also adjacent only: structured provider/model/tool causes may be exposed to Patrol and Assistant, but they do not grant agent install, enrollment, - or fleet command authority. + or fleet command authority. Assistant runtime identity strings exposed by + those handlers must name the first-party surface as Pulse Assistant rather + than the legacy generic `Pulse AI`, and lifecycle consumers must not treat + that label as agent registration, install progress, or command-agent + readiness state. Diagnostics from `internal/api/diagnostics.go` follow the + same adjacent boundary: lifecycle consumers may read + `assistantRuntimeConnected`, but they must not reinterpret it as MCP + transport state, agent registration, install progress, or command-agent + readiness. + Profile-suggestion availability errors consumed through + `frontend-modern/src/api/agentProfiles.ts` are shared Pulse Intelligence + availability guidance, not lifecycle registration state; the client must + point operators at Assistant & Patrol settings instead of reviving `Pulse + Assistant settings` copy. Hosted handoff subjects consumed through the shared API auth boundary must already be stable, non-email principals; lifecycle-adjacent routes must not recover authority from a blank handoff subject by falling back to contact @@ -653,7 +742,16 @@ instead of lifecycle-local centered icon/text shells. `internal/api/ai_handlers.go` and `internal/api/chat_service_adapter.go` may be invoked by lifecycle-adjacent surfaces, but AI runtime auth and provider selection remain `ai-runtime` plus `api-contracts` concerns rather - than install-token or lifecycle credential state. Lifecycle flows must not + than install-token or lifecycle credential state. Legacy Anthropic OAuth + fields exposed by AI settings are cleanup-only compatibility state: + lifecycle-adjacent setup, install, profile, and relay flows must not treat + stored OAuth tokens or `auth_method=oauth` as an agent enrollment, provider + bootstrap, or AI provider-readiness signal. API-facing Assistant chat tool + calls projected through `internal/api/chat_service_adapter.go` must stay + on the shared `agentcapabilities` provider-call shape; lifecycle consumers + must not treat Assistant transcript tool-call IDs, inputs, output, success + flags, or provider continuation metadata as enrollment, assignment, installer, + or fleet-command evidence. Lifecycle flows must not recreate the retired Patrol quickstart bootstrap path, mint server-issued hosted-model tokens, or derive AI provider state from installation identity. Per-request `/api/ai/chat` execution-mode overrides follow that same @@ -756,13 +854,36 @@ instead of lifecycle-local centered icon/text shells. 4. Keep shared agent-side TLS identity fail-closed across `cmd/pulse-agent/main.go`, `internal/hostagent/`, `internal/agentupdate/`, `internal/remoteconfig/client.go`, and `internal/agenttls/config.go`. Self-signed deployments may use a canonical pinned Pulse server certificate fingerprint, but lifecycle transport must route that pin through reporting, enrollment, command websocket, remote-config, and self-update clients instead of widening `PULSE_INSECURE_SKIP_VERIFY` into a blanket MITM carve-out. A configured custom CA bundle is part of that same trust boundary: if the bundle is unreadable or invalid, lifecycle transport must refuse the connection path rather than silently downgrading back to system roots. 5. Keep release-grade updater trust fail-closed across `internal/agentupdate/`, `internal/dockeragent/`, and the shared `internal/api/unified_agent.go` download helpers. When release builds embed trusted update signing keys, published agent binaries and installer assets must carry detached `.sig` plus `.sshsig` sidecars; updater/runtime paths must require `X-Signature-Ed25519` in addition to `X-Checksum-Sha256`, and installer-owned download flows must require the matching base64-encoded `X-Signature-SSHSIG`, instead of silently downgrading to checksum-only trust. 6. Keep shared `internal/api/` helper edits isolated from agent lifecycle semantics: Patrol-specific status transport or alert-trigger wiring changes in shared handlers must not bleed into auto-register, installer, or fleet-control behavior unless this contract moves in the same slice. - The same isolation rule applies to AI settings payload work in `internal/api/ai_handlers.go`: provider auth fields, masked-secret echoes, and provider-test model selection remain AI/runtime plus API-contract ownership and must not be reinterpreted as lifecycle setup or registration semantics just because they share backend helper layers. + The same isolation rule applies to AI settings payload work in `internal/api/ai_handlers.go`: provider auth fields, masked-secret echoes, provider-test model selection, and legacy Anthropic OAuth cleanup fields remain AI/runtime plus API-contract ownership and must not be reinterpreted as lifecycle setup, provider activation, or registration semantics just because they share backend helper layers. + Patrol readiness labels on the same settings payload, including the + user-facing Patrol control label for the stable `configuration` check ID, + remain AI/runtime plus API-contract wording and must not be treated as agent + lifecycle configuration, install readiness, registration state, or fleet + control setup. + Governed AI action-target normalization in + `internal/api/ai_handlers.go` and `internal/api/ai_resource_types.go` + follows that same split: Assistant approval routing may map resource types + to action targets, but lifecycle setup, enrollment, installer, and command + websocket target semantics remain owned by the lifecycle contracts above. The same isolation rule applies to Pulse Assistant chat SSE progress in `internal/api/ai_handler.go`: neutral `workflow_state` transport liveness such as `stream_idle`, provider startup, retry, fallback, and model-thinking status remains AI/runtime plus API-contract ownership and must not be reinterpreted as agent enrollment, install progress, command websocket liveness, or fleet-control freshness. + Native Assistant workflow-prompt rendering through + `POST /api/ai/workflow-prompts/render` follows the same split: manifest-owned + prompt names, argument validation, and rendered starter text remain + AI/runtime plus API-contract ownership and must not become lifecycle setup, + enrollment, installer, or fleet-control state just because the route lives in + `internal/api/ai_handler.go`. + Workflow-starter activity telemetry recorded after successful Assistant + prompt rendering, first-party Patrol activation handoff, Pro activation + handoff, or Pulse MCP prompt rendering follows the same ownership split. The + marker may contribute to Pulse Intelligence activation reporting, but + lifecycle code must not treat it as proof of installed agents, reported + hosts, command connectivity, stale-agent freshness, or fleet-operation + capability. The same isolation rule applies to report branding validation and rendering request assembly in `internal/api/system_settings.go` and `internal/api/metrics_reporting_handlers.go`: lifecycle-owned install, @@ -832,6 +953,45 @@ instead of lifecycle-local centered icon/text shells. valid for one authenticated session, but retained CSRF hashes are not install tokens, setup-token state, enrollment authority, or agent credential continuity. + The same isolation rule applies to the Pulse Intelligence agent capability + manifest in `internal/agentcapabilities/manifest.go`: lifecycle surfaces may consume infrastructure + setup routes advertised there, but manifest governance fields, MCP tool + projection, shared external-tool projection helpers, shared schema-envelope + helpers, and external-agent typed argument schemas remain API/AI-owned + contract state rather than agent enrollment or installer semantics. + Native Pulse Assistant provider seams and native tool-adapter names in + shared `internal/api/ai_handler.go`, `internal/api/ai_handlers.go`, + `internal/api/agent_profiles_tools.go`, `internal/api/router.go`, and + `internal/api/router_routes_ai_relay.go` + follow that same isolation rule: `MCP` remains an external protocol, + manifest, and wire-schema term, while the in-app Assistant tool family is + AI/runtime plus API-contract state. Lifecycle consumers must not infer + enrollment, installer, command-websocket, or fleet-control authority from + those native Assistant tool names, and must not reintroduce MCP-named native + adapters to express lifecycle ownership. + Pulse Intelligence operations-loop external-agent readiness in + `internal/api/agent_resource_context.go` follows that same shared-helper + split: the Pulse MCP token/scope check is AI/runtime plus API-contract + ownership and must not become agent enrollment, installer, fleet-control, or + command-websocket readiness. + Patrol control completed/resolved proof and `patrolControlValueState` + follow the shared `internal/telemetry` count-only classifier even when they + are surfaced by `internal/api/agent_resource_context.go`; legacy + `patrolAutonomy*` fields mirror those values only for compatibility, and + lifecycle surfaces may observe them as operations-orientation evidence only. + `proActivationOperationsLoopStarterCount` is only legacy entry-point + orientation, and the legacy completed/resolved/value `proActivation*` status + fields mirror Patrol control values as compatibility aliases, not as a second + lifecycle signal. MCP readiness remains a separate + external-agent handoff signal and is not a Patrol control proof input, + agent install, registration, update, profile, command, or fleet-control + completion. + Approved Assistant tool execution exposed through the same shared + `internal/api/router_routes_ai_relay.go` extension point follows that native + naming rule: `AssistantToolExecutor` / `ApprovedAssistantToolExecutor` is the + only current cross-repo fix-execution dependency. MCP remains an external + protocol adapter term and must not reappear as a lifecycle command ownership + or approved-fix execution dependency. The same shared-helper rule now covers SSO outbound discovery and metadata fetches plus credential-file loads in `internal/api/sso_outbound.go`, `internal/api/saml_service.go`, and `internal/api/oidc_service.go`: lifecycle-adjacent setup or auth work may depend on that shared trust boundary, but it must not fork a second HTTP client, redirect policy, file-read rule, or SSO entitlement interpretation inside lifecycle-local flows. SAML and multi-provider SSO availability remains API/security-owned Community-tier behavior, not an agent lifecycle setup gate. The same shared-helper rule also covers organization membership and cross-organization sharing transport in `internal/api/org_handlers.go` plus @@ -1292,6 +1452,18 @@ Agent` secondary handoff against the live setup wizard instead of relying ## Current State +Denied Patrol investigation-fix approvals passing through shared +`internal/api/` handlers are adjacent AI-runtime/action-governance state only. +The `fix_rejected` finding outcome records that an operator declined a proposed +Patrol fix before execution; it must not be reinterpreted as agent enrollment, +install progress, fleet liveness, or an agent-command failure. + +Patrol finding lifecycle payloads exposed through shared AI handlers, including +operator resolution-note fields, remain AI-runtime/API-contract vocabulary: +adjacent fleet and install surfaces may render the lifecycle state when another +surface supplies it, but they must not reinterpret it as install progress, +agent approval state, topology truth, or lifecycle-owned remediation policy. + Default-org token scoping and notification-settings fan-out on shared `internal/api/` handlers are likewise adjacent only: org-bound token denial for the default org and instance-wide webhook allowlist propagation are @@ -1363,6 +1535,14 @@ unauthenticated and cacheable; the underlying capabilities keep their own auth scopes. Adding a capability is a deliberate "this is part of the agent surface" commitment so the agent contract stays curated, not auto-derived from every internal endpoint. +The manifest's action mode and approval policy metadata are API/AI-owned +governance posture; lifecycle setup, enrollment, and fleet-control flows may +surface that metadata to agents but must not reinterpret it as install, +registration, heartbeat, update, or agent authority. +The same adjacency applies to manifest `inputSchema` metadata: typed tool +arguments help external agents call API-owned actions, but do not create a +lifecycle-owned setup command path, operator-state write contract, or +agent-control argument registry. `/api/agent/resource-context/{id}` is the agent-paradigm substrate endpoint: any agent (in-process Patrol/Assistant or external) reads @@ -1409,6 +1589,12 @@ The handlers were migrated from the platform-wide `APIError` envelope to the agent-stable `{"error", "message", "details"?}` shape so the substrate keeps a single envelope contract across read, write, and action capabilities. +The governed Patrol finding lifecycle tools advertised in the same +manifest (`acknowledge_finding`, `snooze_finding`, `dismiss_finding`, +`resolve_finding`) also return that agent-stable envelope for branchable +failures and declare their closed error-code set in the manifest. That is +an API/AI-runtime contract only: it does not create agent lifecycle state, +registration state, install state, or any new durable recovery artifact. Docker / Podman lifecycle execution extends that same action category only: the manifest advertises the `execute_action` substrate, while per-resource container capabilities, policy checks, action audit records, and terminal @@ -1748,6 +1934,94 @@ setup or router work touches shared `internal/api/` files, telemetry preview and install-ID reset routes must keep reusing the canonical system-settings trust boundary and server-owned telemetry runtime instead of borrowing agent lifecycle proof or state ownership just because the same router surface moved. +Content-free Pulse Intelligence telemetry rollups under shared `internal/api/` +also remain system-settings, API-contract, and security/privacy ownership: +lifecycle-adjacent surfaces may observe that action-plan, approval, +approved-action-decision, rejected-action-decision, first-party workflow +starter, Pro activation starter, and external-agent usage is summarized, but +they must not reinterpret those anonymous counters as agent enrollment, install +success, recovery scope, or lifecycle state. +External-agent activity may be counted when a narrow API token satisfies the +specific manifest capability scope being called, including read-only +`monitoring:read` context usage. That signal remains API-owned collaboration +telemetry, not evidence that a command agent installed, checked in, or applied +fleet state. +External-agent/MCP readiness may also be true for a non-expired token that +covers any Pulse MCP-published capability scope, but that readiness remains a +collaboration-surface setup signal. Agent lifecycle code must not reinterpret +it as host-agent enrollment, command-agent reachability, or applied fleet +configuration. The manifest shape alone may prove that Pulse can publish the +external-agent contract, but it must not satisfy `externalAgentReady` on +`/api/agent/patrol-control/status` without a current token that covers at +least one Pulse MCP surface capability. The legacy +`/api/agent/operations-loop/status` URL is only a compatibility alias. Missing +external-agent readiness must not downgrade or block first-party Patrol control +completed/resolved proof; the legacy `verified_needs_mcp` state is retained +only as compatibility input and must not become new lifecycle or activation +output. +The Patrol-control status projection at +`GET /api/agent/patrol-control/status` follows the same ownership split. Its +stage labels, next action, Patrol issue evidence count, pending approval count, +contextual collaboration count, governed action count, verified outcome count, +Patrol control starter/completed/resolved proof exposed first through +`patrolControl*` fields and mirrored through `patrolAutonomy*` compatibility +fields, +`proActivationOperationsLoopStarterCount` as legacy entry-point orientation, +legacy completed/resolved/value `proActivation*` aliases, and optional +external-agent readiness +are content-free Pulse Intelligence collaboration signals; lifecycle code must +not reinterpret them as installed-agent proof, command-agent heartbeat, +installer progress, profile convergence, or fleet-control authority. +When that projection lets an aggregate active Patrol finding or pending approval +outrank older completed/resolved loop proof, the result is still operator +orientation only; it is not host-agent liveness, install health, or command +authorization evidence. +The same route may use action lifecycle events to notice recent loop activity, +but its governance and verification counts remain action-governance signals: +`governedActionCount` requires approved or rejected governed-action evidence, +`approvedDecisionCount` and `rejectedDecisionCount` expose that split without +identifiers, and `verifiedOutcomeCount` requires an approved governed action +with verified post-action evidence. Agent lifecycle surfaces must not satisfy +those stages from generic `executing`, `completed`, or `failed` command-agent +state, and must not reinterpret a rejected-only no-execution terminal decision +as command-agent success or failure. +The same verified-outcome predicate backs outbound Pulse Intelligence +approved-success telemetry: a completed action result is not lifecycle proof +unless the approved action also has `VerificationOutcome.Status=verified` or a +canonical verification result that ran and succeeded. Agent lifecycle surfaces +must not count host-agent command completion, installer success, or generic +execution success as Patrol control resolved-loop proof. +The route's four-step operator rollup follows the same boundary: governance +step counts may represent pending approvals before a decision or +approved/rejected decision evidence after one exists, while verification step +counts may represent verified outcomes or terminal rejected decisions. Optional +MCP readiness stays in `externalAgentReady`, not in the operator step list. +Agent lifecycle surfaces must not read those step counts as host-agent +heartbeat, command-agent install state, or fleet-control authority. +Approved action decision telemetry may use action lifecycle events or approved +approval records as its API-owned proof source, but the exported value remains +an anonymous approve/reject journey counter. Agent-lifecycle surfaces must not +read that rollup as command dispatch, command-agent reachability, profile +convergence, install success, or fleet lifecycle state. +Approved execution attempt telemetry may use action lifecycle events as its +API-owned proof source, but the exported value remains an anonymous operations +loop counter. Agent-lifecycle surfaces must not treat that rollup as command +agent heartbeat, installation proof, profile convergence, or fleet lifecycle +state. +Approved action success telemetry may also derive from the same governed action +audit stream, but only as a content-free count of approved actions that reached +completed state with a successful result. The existing approved execution +counter remains attempt-based for compatibility; the success counter must not +export resource identifiers, actor identifiers, command text, command output, +verification details, or host-agent state. +The external-agent recent-use bit is backed by content-free authenticated +agent/MCP route activity for manifest-capable API tokens; it is not an agent +lifecycle heartbeat, install proof, or generic token-last-used proxy. +The MCP adapter recent-use bit is narrower adapter-origin telemetry for +`pulse-mcp` requests on that same shared agent surface. It may distinguish MCP +adapter collaboration from direct external-agent API collaboration, but agent +lifecycle surfaces must not treat it as evidence that `pulse-agent` installed, +checked in, accepted remote config, or executed a command. That same shared `internal/api/ai_handlers.go` dependency also now assumes Patrol-specific settings and status expansions stay Patrol-owned. When shared AI handlers add split scoped-trigger fields, recency labels, or trigger-state diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 2e37b5415..ff6a7a4c9 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -9,51 +9,911 @@ "contract_file": "docs/release-control/v6/internal/subsystems/ai-runtime.md", "status_file": "docs/release-control/v6/internal/status.json", "registry_file": "docs/release-control/v6/internal/subsystems/registry.json", - "dependency_subsystem_ids": ["api-contracts", "cloud-paid", "frontend-primitives"] + "dependency_subsystem_ids": [ + "api-contracts", + "cloud-paid", + "frontend-primitives" + ] } ``` ## Purpose -Own Pulse Assistant and Patrol backend runtime behavior, AI orchestration, -runtime cost control, shared AI transport surfaces, and browser-visible -Assistant transcript actions that define what visible operator/model text can -leave the transcript without exposing hidden provider/tool metadata. +Own the Pulse Intelligence Core: canonical context, governed actions, safety +gates, approval state, action audit, and verification. That core backs Pulse +Patrol as the primary built-in operator that watches, investigates, acts within +the chosen Patrol mode, verifies outcomes, and records what happened. Pulse +Assistant is the contextual +explanation, approval, and handoff access path over that same work, while the +Pulse MCP adapter is the external-agent access path over canonical API +contracts. The subsystem also owns AI orchestration, runtime cost control, +shared AI transport surfaces, and browser-visible Assistant transcript actions +that define what visible operator/model text can leave the transcript without +exposing hidden provider/tool metadata. +When backend Patrol summaries or Assistant handoffs expose run coverage to the +browser, they translate internal full-run, scoped-run, and verification +precision into plain operator check language rather than activation-loop, +proof-strip, or scoped/full-run jargon. +Coverage warnings must stay action-oriented: they may tell the operator to run +Patrol to check everything, but must not ask for a "full issue list" or otherwise +expose backend full-run vocabulary as the product instruction. +Patrol model-facing system prompts must derive the active control-mode block +from the effective `patrol_autonomy_level`, after license and full-mode-lock +clamping, so Watch only, Ask first, Safe auto-fix, and Autopilot give the model +the same investigate, approval, execution, and verification boundaries enforced +by the orchestrator. Prompt copy must not fall back to the legacy +`patrol_auto_fix` boolean or the retired "observe only" / "auto-fix mode" +framing. +Patrol finding storage must consolidate equivalent active storage-capacity +siblings before they reach browser surfaces: a broader storage risk and a +generic usage/capacity finding for the same normalized storage identity are one +operator issue, while distinct backup, health, and pool-vs-device findings +remain separate. This consolidation belongs in the finding store, not the +frontend row renderer. + +The public Pulse Intelligence overview is a projection of those runtime +contracts, not a second Assistant tool inventory. It must point readers at the +registry-owned Assistant tool governance and the manifest-owned external-agent +capabilities instead of carrying hand-maintained tool tables. +It is also the public product-language contract for paid Patrol capabilities: +Pro positioning must describe hands-on Patrol modes, issue investigation, +governed fixes, verified outcomes, and history. Public docs and generated +commercial copy must not reintroduce proof-first activation-loop framing or the +retired `Patrol Control Levels`, `Patrol control`, and `Alert investigation` +labels as the visible paid story. API fields and route markers may retain +compatibility names such as `autonomy_level` only when the surrounding copy +plainly calls the operator-facing choice `Patrol mode`. +Its Core/Patrol/Assistant/MCP relationship block must be rendered from +`PulseIntelligenceOverviewMarkdown(Manifest.SurfaceContract)` rather than +maintained as prose separate from the agent capabilities manifest. +Reusable workflow starters are manifest-owned `workflowPrompts` metadata +derived from the shared Pulse Intelligence workflow-prompt core. Prompt names, +Assistant display labels, presentation kind hints, descriptions, arguments, +availability, and rendered text belong to that shared catalogue. Pulse +Assistant-compatible starters, frontend manifest clients, MCP `prompts/list`, +and generated docs consume that catalogue instead of maintaining per-surface +prompt lists or browser-local starter copy. +The shared catalogue includes the global `pulse_operations_loop` workflow +starter for the Patrol mode operations loop: it is projected only when the +manifest has fleet context, resource context, findings, governed +plan/decide/execute action, and finding resolution capabilities. Its rendered +prompt must keep the operator in an approve-or-reject loop with execution only +after policy allows it and verification before finding resolution, while the +status schema treats Patrol mode starter evidence only as entry-point +orientation, Patrol mode completed-loop evidence as aggregate terminal +approve/reject proof, and Patrol mode resolved-loop evidence only as +aggregate approved-and-verified outcome proof, not Assistant transcript, Patrol +finding identity, approval payload, action command, or MCP setup detail. +`pulse_patrol`, the current paid `patrol_control` marker, the legacy +`patrol_autonomy` marker, and the legacy `pulse_pro_activation` alias all +contribute to first-party Patrol mode starter evidence; +`proActivationOperationsLoopStarterCount` is a compatibility field retained +for older external-agent clients, while primary clients must read +`patrolControlOperationsLoopStarterCount`. The other `proActivation*` status +fields are compatibility aliases for the same first-party Patrol mode +journey rather than a separate AI-runtime loop, and manifest descriptions must +not describe them as a Pro activation journey. +The same status schema must expose the aggregate Patrol mode value state as +a content-safe enum so Assistant-compatible and MCP-facing agents can branch +without reverse-engineering counts. `governed_decision_recorded` is a safe +terminal decision state, not proven operations value; `verified` is the only +completed first-party value state and requires the approved, verified outcome +plus recorded action history. MCP/external-agent readiness remains optional +external-agent setup context. +Native Assistant workflow starters in `frontend-modern/src/components/AI/Chat/` +are a browser projection of that same catalogue, not a second prompt registry: +they may decide which manifest-owned prompt is contextually usable, but display +labels and rendered prompt text must come from shared manifest metadata and the +shared backend renderer on +`POST /api/ai/workflow-prompts/render`. ## Canonical Files 1. `internal/ai/` + 1a. `cmd/pulse-mcp/main.go` + 1b. `cmd/pulse-mcp/README.md` + 1c. `internal/agentcapabilities/` + 1d. `scripts/generate-pulse-intelligence-docs.go` 2. `internal/config/ai.go` 3. `internal/api/ai_handler.go` 4. `internal/api/ai_handlers.go` 5. `internal/api/ai_hosted_runtime.go` 6. `internal/api/ai_intelligence_handlers.go` -7. `frontend-modern/src/api/ai.ts` -8. `frontend-modern/src/api/aiChat.ts` +7. `frontend-modern/src/api/agentCapabilities.ts` + 7a. `frontend-modern/src/api/generated/agentCapabilities.ts` +8. `frontend-modern/src/api/ai.ts` + 8a. `frontend-modern/src/api/aiChat.ts` 9. `frontend-modern/src/api/patrol.ts` 10. `frontend-modern/src/components/AI/AICostDashboard.tsx` -11. `frontend-modern/src/components/AI/Chat/` -12. `frontend-modern/src/utils/aiChatPresentation.ts` -13. `frontend-modern/src/utils/aiControlLevelPresentation.ts` -14. `frontend-modern/src/utils/aiCostPresentation.ts` -15. `frontend-modern/src/utils/aiProviderHealthPresentation.ts` -16. `frontend-modern/src/utils/aiProviderPresentation.ts` -17. `frontend-modern/src/utils/textPresentation.ts` -18. `frontend-modern/src/stores/aiRuntimeState.ts` -19. `frontend-modern/src/stores/aiChat.ts` -20. `docs/AI.md` -21. `pkg/aicontracts/investigation.go` +11. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` +12. `frontend-modern/src/components/AI/Chat/` +13. `frontend-modern/src/utils/aiChatPresentation.ts` +14. `frontend-modern/src/utils/aiControlLevelPresentation.ts` +15. `frontend-modern/src/utils/aiCostPresentation.ts` +16. `frontend-modern/src/utils/aiProviderHealthPresentation.ts` +17. `frontend-modern/src/utils/aiProviderPresentation.ts` +18. `frontend-modern/src/utils/textPresentation.ts` +19. `frontend-modern/src/stores/aiRuntimeState.ts` +20. `frontend-modern/src/stores/aiChat.ts` +21. `docs/AI.md` +22. `pkg/aicontracts/investigation.go` +23. `pkg/aicontracts/orchestrator_deps.go` +24. `pkg/aicontracts/fix_execution.go` +25. `pkg/extensions/ai_autofix.go` ## Shared Boundaries -1. `frontend-modern/src/api/ai.ts` shared with `api-contracts`: the AI frontend client is both an AI runtime control surface and a canonical API payload contract boundary. -2. `frontend-modern/src/api/patrol.ts` shared with `api-contracts`: the Patrol frontend client is both an AI runtime control surface and a canonical API payload contract boundary. -3. `frontend-modern/src/stores/aiChat.ts` shared with `frontend-primitives`: the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary. -4. `internal/api/ai_handler.go` shared with `api-contracts`: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary. -5. `internal/api/ai_handlers.go` shared with `api-contracts`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary. -6. `internal/api/ai_intelligence_handlers.go` shared with `api-contracts`: AI intelligence handlers are both an AI runtime control surface and a canonical API payload contract boundary. +1. `cmd/pulse-mcp/main.go` shared with `api-contracts`: the Pulse MCP adapter runtime is both an AI runtime surface for external-agent access to Pulse Intelligence and a canonical API contract projection over the agent capabilities manifest and Pulse MCP surface tool contract. +2. `cmd/pulse-mcp/README.md` shared with `api-contracts`: the Pulse MCP adapter guide is both an AI runtime surface for external-agent access to Pulse Intelligence and a canonical API contract projection over the agent capabilities manifest. +3. `frontend-modern/src/api/agentCapabilities.ts` shared with `api-contracts`: the agent capabilities frontend client is both the Pulse Intelligence external-agent manifest consumer and a canonical API payload contract boundary. + Its presentation sanitizer may translate legacy Pro activation manifest + descriptions into Patrol mode outcome/status language, but it must not + reintroduce proof-first or activation-loop copy into Assistant, Patrol, or + external-agent onboarding surfaces. +4. `frontend-modern/src/api/ai.ts` shared with `api-contracts`: the AI frontend client is both an AI runtime control surface and a canonical API payload contract boundary. +5. `frontend-modern/src/api/aiChat.ts` shared with `api-contracts`: the Assistant chat frontend client is both the first-party Assistant transport surface and a canonical API payload contract boundary. +6. `frontend-modern/src/api/generated/agentCapabilities.ts` shared with `api-contracts`: the generated agent capabilities frontend types are both the Pulse Intelligence manifest TypeScript projection and a canonical API payload contract boundary. +7. `frontend-modern/src/api/patrol.ts` shared with `api-contracts`: the Patrol frontend client is both an AI runtime control surface and a canonical API payload contract boundary. +8. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` shared with `api-contracts`, `frontend-primitives`: the External agents settings panel is the optional settings-shell projection of Pulse MCP onboarding, the AI runtime connected-agent onboarding surface, and a presentation consumer of the shared agent capabilities frontend client. + Its default copy must frame connected clients as optional connector access + to Pulse context and Patrol work, with Patrol as the operator that watches, + acts within Patrol mode, asks for approval when required, verifies outcomes, + and records history. The normal Assistant settings view must keep setup + mechanics behind a `Show connector setup` disclosure, with direct setup + links opening that disclosure automatically. Default copy must make clear + that connected tools do not get separate powers. Tool-contract posture and manifest facts are + client-builder diagnostics and must stay behind a Developer details + disclosure rather than beside the normal external-agent setup steps; posture + and policy context belongs under Patrol access model, while prompt, scope, + failure-code, and tool inventories must sit one level deeper behind Live + manifest details using user-facing labels such as Agent starting points and + Agent capabilities. + Direct links to + `/settings/pulse-intelligence/assistant#external-agent-setup` must focus this + External agents panel after the Assistant settings layout settles, + because connected agents are optional access to Patrol work and should not + strand users at the generic token inventory. Legacy + `/settings/security/api#external-agent-setup` and + `/settings/security/api#pulse-mcp-setup` links must remain accepted and + redirect to the canonical Pulse Intelligence Assistant route. + The `Choose Patrol mode` handoff must target the Patrol operator surface at + `/patrol#patrol-control`, where the mode is configured, while API + Access remains the token and external-client setup surface. The expanded + setup checklist owns that handoff as Step 1; the panel header must not repeat + a second visible `Choose Patrol mode` action while setup is open. + The visible full-surface token preset for that setup is `Patrol external agent`; + `pulse_intelligence_agent` remains only the compatibility preset id used by + the route and token model. +9. `frontend-modern/src/stores/aiChat.ts` shared with `frontend-primitives`: the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary. +10. `internal/agentcapabilities/action_target.go` shared with `api-contracts`: the Pulse Intelligence governed action target type and resource-to-action-target mapping vocabulary are both the Assistant approval/runtime routing contract and the canonical API/agent target contract for governed actions. +11. `internal/agentcapabilities/control_level.go` shared with `api-contracts`: the Pulse Intelligence control-level vocabulary and control-tool availability predicate are both the Assistant runtime gating contract and the canonical API/agent permission posture for governed action tools. +12. `internal/agentcapabilities/errors.go` shared with `api-contracts`: the Pulse Intelligence agent error envelope is both the canonical API failure payload contract and the AI runtime adapter error-parsing contract for Assistant and external-agent surfaces. +13. `internal/agentcapabilities/events.go` shared with `api-contracts`: the Pulse Intelligence event vocabulary is both the canonical API SSE event contract and the AI runtime adapter notification contract for Assistant and external-agent surfaces. +14. `internal/agentcapabilities/governance_prompt.go` shared with `api-contracts`: the Pulse Intelligence surface-affordance-resolved model-facing operating-instruction, tool-governance prompt, reusable provider-tool governance description, Assistant-native offered-tool filtering, and Assistant-native interactive question-tool governance projections are both the Assistant system-prompt governance section and the shared API/agent vocabulary for action mode, approval posture, MCP affordance advertisement, and non-registry interaction-tool boundaries. +15. `internal/agentcapabilities/http.go` shared with `api-contracts`: the Pulse Intelligence agent HTTP substrate is both the API capabilities invocation contract and the shared AI runtime adapter execution primitive for MCP and reference agent clients. +16. `internal/agentcapabilities/manifest.go` shared with `api-contracts`: the canonical Pulse Intelligence agent capabilities manifest declaration, including capability display titles, manifest-owned finding lifecycle schemas, manifest-owned governed action schemas and routes, manifest-owned external-adapter surface tool contracts, and manifest-owned structured output schemas, is both the API discovery payload source and the AI runtime projection contract for Pulse Assistant and MCP-facing agent tools. +17. `internal/agentcapabilities/markdown.go` shared with `api-contracts`: the Pulse Intelligence manifest Markdown projection, including manifest-owned capability titles, surface-filtered Pulse MCP tool/error inventories, and prompt labels, is both the canonical API/agent documentation projection and the AI runtime onboarding projection for Assistant-compatible external-agent surfaces. +18. `internal/agentcapabilities/mcp.go` shared with `api-contracts`: the Pulse Intelligence MCP protocol version, JSON-RPC, method dispatch, method payload, surface-tool-contract-gated initialize operating-instruction and capability advertisement payload, manifest surface-filtered tools/list and tools/call execution bridge, manifest surface-gated resources/list and resources/read bridge, manifest-owned and surface-affordance-gated workflow prompt projection, protocol wire aliases, resource and prompt handler gates, and notification projection collectively define the external-agent adapter wire contract over the shared Pulse Intelligence tool core; MCP initialize, tools/call execution, resource list/read projection, and prompt list/get projection must enter through manifest-owned surface and workflow-prompt contracts so raw capability slices cannot bypass the published external-adapter contract. +19. `internal/agentcapabilities/mcp_adapter.go` shared with `api-contracts`: the Pulse MCP adapter setup contract defaults and normalization are both the canonical API manifest setup projection and the AI runtime onboarding contract for Assistant-compatible external-agent surfaces. +20. `internal/agentcapabilities/projection.go` shared with `api-contracts`: the agent capability external-tool projection helper, normalized manifest-owned surface tool contract resolution and tools-affordance gating, manifest-owned resource-context route and argument vocabulary, operator-state capability and route vocabulary, finding workflow capability and lifecycle argument vocabulary including resolution and dismissal notes, governed action capability, route, and argument vocabulary, manifest-owned tool title and outputSchema projection, structured Pulse capability _meta, and shared tool behavior hints are both the canonical API manifest projection contract and the AI runtime adapter projection for Pulse Assistant and MCP-facing agent tools, with MCP annotation and metadata wire names confined to adapter-edge aliases. +21. `internal/agentcapabilities/provider_tool_artifacts.go` shared with `api-contracts`: the provider tool-call artifact detector and streaming tool-name prefix splitter are both the Assistant stream-sanitization boundary and the shared external-adapter leak guard for provider-native tool-call markup that escaped the structured channel. +22. `internal/agentcapabilities/schema.go` shared with `api-contracts`: the agent capability input schema contract is both the canonical API manifest schema envelope and the AI runtime structured tool-schema, governance-aware provider-projection with neutral behavior hints and Pulse governance metadata, offered-tool governance extraction for Assistant prompt policy, manifest-affordance-gated Assistant provider-surface composition, manifest raw-schema to Assistant provider-schema projection for capability tools, legacy native Assistant utility provider aliases and schemas, provider-call normalization, provider-result context projection, Assistant-native interaction provider-tool declaration, and live Assistant execution-normalization contract for Pulse Assistant and MCP-facing agent tools. +23. `internal/agentcapabilities/scopes.go` shared with `api-contracts`: the manifest-derived required-scope summary is both the canonical API/agent token guidance contract and the AI runtime adapter startup/onboarding contract for Assistant-compatible external-agent surfaces. +24. `internal/agentcapabilities/sse.go` shared with `api-contracts`: the Pulse Intelligence SSE subscription transport and record parser are both the canonical API event-stream consumption contract and the AI runtime adapter push bridge contract for MCP and reference agent clients. +25. `internal/agentcapabilities/surface_contract.go` shared with `api-contracts`: the Pulse Intelligence operator-surface affordance contract, shared surface-affordance, surface-tool identity, Assistant surface tool filtering, normalized external surface tool resolver, surface lookup, affordance labels, and manifest-published external-adapter surface tool allowlist projection are both the canonical API manifest surface model and the AI runtime prompt and onboarding guardrail for Assistant and MCP-facing surfaces. +26. `internal/agentcapabilities/text_tool_invocation.go` shared with `api-contracts`: the Pulse Intelligence text tool invocation parser, internal approval argument, and current_resource handle vocabulary are both the Assistant approved-action execution projection and the shared tool-call params bridge for governed Pulse Intelligence tool calls, with MCP tools/call compatibility staying at the adapter edge. +27. `internal/agentcapabilities/tool_call.go` shared with `api-contracts`: the Pulse Intelligence shared tool-call params, normalization, validation, direct registry preparation, registry-entrypoint failure result helpers, and provider/registry tool-call safety classification are both the native Assistant execution/FSM contract and the canonical API/agent tools/call compatibility contract for governed Pulse Intelligence tool calls. +28. `internal/agentcapabilities/tool_execution.go` shared with `api-contracts`: the Pulse Intelligence neutral capability tool HTTP execution helper and direct tool execution output/error mapper are both the Assistant-native direct execution contract and the canonical API/agent request/response execution contract, with MCP adapters consuming the neutral helpers only after the shared MCP manifest-surface execution bridge has applied the published surface tool contract. +29. `internal/agentcapabilities/tool_marker.go` shared with `api-contracts`: the Pulse Intelligence Assistant tool marker vocabulary and approval/policy marker parser are both the Assistant structured tool-result compatibility contract and the canonical API/agent branching contract for governed tool outcomes. +30. `internal/agentcapabilities/tool_names.go` shared with `api-contracts`: the Pulse Intelligence registry tool-name vocabulary is both the native Assistant execution/display contract and the canonical API/agent tool identity contract for MCP-facing external-agent adapters. +31. `internal/agentcapabilities/tool_response.go` shared with `api-contracts`: the shared tool response envelope, tool error-code vocabulary, and tool-result error-code and verification evidence parsers are both the Assistant structured tool-result contract and the canonical API/agent branching contract for Pulse Intelligence tool failures, recovery tracking, and write self-verification. +32. `internal/agentcapabilities/tool_result.go` shared with `api-contracts`: the Pulse Intelligence shared tool-result content/result envelope, structuredContent projection, result constructors, HTTP response-to-result mapping, text projection, and result interpretation helpers are both the Assistant registry result contract and the canonical API/agent result projection contract for governed tool outcomes. +33. `internal/agentcapabilities/types.go` shared with `api-contracts`: the agent capabilities manifest wire type, manifest-owned external-adapter surface tool contract field, capability display title and structured output schema fields, approval-policy vocabulary, capability governance normalization, and tool-governance descriptor shape are both the canonical API payload contract and the AI runtime projection contract for Pulse Assistant and MCP-facing agent tools. +34. `internal/agentcapabilities/workflow_prompt.go` shared with `api-contracts`: the Pulse Intelligence workflow prompt catalogue, manifest-owned `workflowPrompts` projection, MCP prompt title projection, presentation kind hints, shared resource-context and finding argument vocabulary, Patrol issue-handling capability gating, argument validation, and manifest-gated shared prompt rendering rules are both the AI runtime starter contract for Assistant-compatible surfaces and the canonical API/agent prompt projection contract for MCP-facing clients. +35. `internal/api/ai_handler.go` shared with `api-contracts`: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary. +36. `internal/api/ai_handlers.go` shared with `api-contracts`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary. +37. `internal/api/ai_intelligence_handlers.go` shared with `api-contracts`: AI intelligence handlers are both an AI runtime control surface and a canonical API payload contract boundary. +38. `pkg/aicontracts/fix_execution.go` shared with `api-contracts`: the public approved-fix execution contract is both an AI runtime approved-action boundary and a canonical API dependency contract for Patrol and enterprise auto-fix binders. +39. `pkg/aicontracts/investigation.go` shared with `api-contracts`: the public Patrol investigation record and finding contract is both an AI runtime handoff boundary and a canonical API payload contract for Patrol, Assistant, unified findings, persistence, and audit surfaces. +40. `pkg/aicontracts/orchestrator_deps.go` shared with `api-contracts`: the public investigation orchestrator dependency contract is both an AI runtime handoff boundary and a canonical API payload contract for Assistant and Patrol tool-call history. +41. `pkg/extensions/ai_autofix.go` shared with `api-contracts`: the enterprise auto-fix extension dependency seam is both an AI runtime approved-action boundary and a canonical API extension contract over Assistant and Patrol execution dependencies. +42. `scripts/generate-pulse-intelligence-docs.go` shared with `api-contracts`: the Pulse Intelligence manifest docs generator is both an AI runtime docs/onboarding projection and a canonical API contract projection over the agent capabilities manifest and Pulse MCP surface tool contract. + +The shared agent-capabilities manifest also owns the runtime surface contract: +the manifest wire type and generated frontend projection must name Pulse +Intelligence Core as the shared core, Pulse Patrol as the primary built-in +operator, and Pulse Assistant plus Pulse MCP as contextual and external-agent +access paths over the same governed capabilities. Frontend copy, MCP docs, and +Assistant onboarding must consume that relationship from the manifest rather +than carrying parallel surface tables. +The same manifest owns Pulse MCP adapter setup facts: server name, command, +base URL flag/default, token environment variable, and supported client config +families. Settings, README generation, and adapter onboarding may present those +facts for operators, but they must not maintain separate MCP setup constants. +The in-app setup surface for those facts belongs under Pulse Intelligence +settings because Pulse MCP is an external-agent access path over governed +Patrol actions. API Access may be linked for scoped-token creation, but it must +not host the external-agent setup walkthrough. +Pulse Intelligence telemetry readiness for the external-agent surface follows +that same manifest surface boundary: a non-expired API token covering any +Pulse MCP-published capability scope is enough to mark MCP/external-agent +readiness, while route-level usage still comes only from authenticated +capability activity markers. Readiness must not be tied to the full manifest +scope set because least-privilege external-agent setups are supported. +The `pulse-mcp` adapter marks its own capability requests with the content-free +`X-Pulse-Agent-Surface: pulse_mcp` header so telemetry can distinguish adapter +use from direct external-agent API use without recording prompts, payloads, +route parameters, token identity, or resource identifiers. +Successful workflow starter rendering follows the same content-free boundary: +native Assistant render calls, first-party paid Patrol autonomy handoffs, and +Pulse MCP `prompts/get` calls may persist only the manifest prompt name, coarse +surface (`pulse_assistant`, `pulse_patrol`, `patrol_autonomy`, legacy +`pulse_pro_activation`, or `pulse_mcp`), and timestamp. The first-party +activation marker is `POST /api/ai/workflow-prompts/activity`; it is an +authenticated `ai:chat` route for app-owned surfaces such as the paid Patrol +autonomy handoff into Patrol, validates prompt names against the manifest +workflow prompt catalogue, accepts only first-party surfaces, and must remain a +starter marker rather than proof that Assistant context, governed action +approval, execution, verification, or finding resolution occurred. +`pulse_pro_activation` exists only as a legacy alias for older Pro +success/current-plan entry points; current paid handoffs use `patrol_autonomy` +and neither surface adds a new prompt, tool, model surface, or operations-loop +completion signal. Prompt arguments, rendered text, resource +IDs, finding IDs, session IDs, token identity, request bodies, and model output +must not enter workflow starter activity history or outbound telemetry. +Runtime model-facing instructions must consume that same contract as data. +`BuildPulseAssistantOperatingInstructions` identifies Pulse Assistant as the +native in-app surface over Pulse Intelligence Core, while manifest-backed MCP +initialize responses identify Pulse MCP as the external-agent adapter over the +same core and Pulse Patrol as the primary built-in operator. That copy belongs in +`internal/agentcapabilities`; Assistant prompts, MCP initialize payloads, and +future agent surfaces must not re-create local Assistant/MCP/Patrol relationship +wording outside the manifest-owned surface contract. +Surface tool summaries follow the same shared-core rule. The native Assistant +runtime exposes provider tools through `ProjectPulseAssistantProviderTools`, +which applies the manifest-owned Pulse Assistant surface affordances before +registry or native interaction tools reach model requests, and exposes +`PulseToolExecutor.AssistantSurfaceToolContract`, which projects the same +runtime-available provider tools through `ProjectPulseAssistantSurfaceToolContract`; +external-agent adapters project request/response manifest capabilities through +`ProjectManifestSurfaceToolContract` and publish the static external-adapter +inventory on `Manifest.SurfaceToolContracts`. Missing external-adapter +`surfaceToolContracts` entries fail closed rather than inferring tools from raw +manifest capabilities; static surface-tool affordance metadata may narrow an +external operator surface, but must not re-enable a disabled surface affordance +or keep tool/capability names when the effective `tools` affordance is false. +Disabled Assistant affordances likewise fail closed rather than allowing runtime +registry availability to re-enable provider tools. +Assistant prompt fallback governance follows that same contract: when no live +executor-owned offered-tool manifest is available, registry-owned fallback +governance must enter through +`CanonicalToolGovernanceForManifestSurface(ControlLevelControlled, CanonicalManifest(), SurfaceIDPulseAssistant)`, +and the chat prompt fallback must derive explicit offered tool names from +`ManifestSurfaceAffordances` so registry tools and the native `pulse_question` +interaction tool cannot be reintroduced by nil offered-tool semantics. +Assistant registry tools, Assistant-native interaction tools, and MCP manifest +capabilities must stay explicit buckets in that contract so Pulse can support +both surfaces without duplicating tool logic or +implying MCP replaces the in-app Assistant. +Frontend consumers must import the generated `SurfaceToolContract` shape from +the shared agent-capabilities client rather than maintaining a local copy. +Patrol work contract availability for browser surfaces is also a shared +agent-capabilities-client verdict: consumers must derive the Pulse MCP contract +from the normalized MCP adapter setup, the manifest-owned +`pulse_operations_loop` workflow prompt, and the Pulse MCP surface tool contract +through `frontend-modern/src/api/agentCapabilities.ts`; feature shells must not +hard-code MCP contract availability or infer it from raw manifest capabilities. +Runtime external-agent readiness is stricter: +`GET /api/agent/patrol-control/status` may mark `externalAgentReady` only when +that contract is available and a single non-expired API token covers every +scope required by the published Pulse MCP Patrol work capability set. +`GET /api/agent/operations-loop/status` remains only a compatibility alias for +older clients. That shared Patrol work tool set starts with +`get_patrol_control_status`, the count-only +`GET /api/agent/patrol-control/status` orientation read, then uses the same +fleet-context, resource-context, finding, governed action, and finding +resolution tools as the rest of the governed issue-handling flow. +`get_operations_loop_status` and `/api/agent/operations-loop/status` are +compatibility aliases, not primary capability names. AI runtime prompts, MCP +readiness helpers, and frontend presentation helpers must not carry separate +Patrol work inventories. The native Patrol work surface may display that same +status projection, but it must fetch it through the shared agent-capabilities +frontend client so Assistant, MCP, and native UI stay on the same Patrol work +contract. The manifest-owned status schema must describe governed counts as +decision-backed, split approved and rejected decision counts explicitly, and +describe verified counts as approved-action-backed. The same schema now exposes +content-free Patrol work starter counts for the total flow and the +Assistant, Patrol mode, older-client compatibility field, and Pulse MCP +source surfaces, an Assistant step count for contextual Assistant or +external-agent collaboration, plus a content-free Patrol mode +completed-loop count derived when Patrol mode starter evidence, Patrol +issue evidence, contextual collaboration, and either a rejected governed +decision or an approved governed decision with verified outcome proof coexist +in the same status window. It also exposes a stricter Patrol mode +resolved-loop count only when that status window has an approved governed +decision and verified outcome proof. The `proActivationOperationsLoopStarterCount` +field is retained only for older clients; primary clients must use +`patrolControlOperationsLoopStarterCount`. The legacy `patrolAutonomy*` and +`proActivation*` completed-loop, resolved-loop, and value-state fields mirror +the Patrol mode counts and value state for compatibility. AI runtime +surfaces may use those counts to show that a starter +was rendered or launched, that contextual collaboration occurred, or that the +first-party Patrol mode +loop reached terminal or resolved proof, but must not treat them as Assistant +transcript, approval payload, action command, resource identity, or +finding-resolution detail. External-agent prompts and MCP adapters may treat a +rejected-only decision as a terminal no-execution outcome, but approved +decisions must still continue through execution and verification before the +verified-outcome stage is complete. +Runtime Assistant availability remains an authenticated Assistant concern, +served by `GET /api/ai/assistant/surface-tools`, not a static fact in the +public agent-capabilities manifest. When the Pulse Assistant shell displays +live capability availability, it must load that authenticated endpoint through +`AIChatAPI.getAssistantSurfaceTools()` and format the posture through +`frontend-modern/src/api/agentCapabilities.ts`; the shell must not keep a local +registry-tool catalogue or reuse MCP manifest capability names as native +Assistant inventory. The External agents settings panel may show the +external Pulse MCP tool posture, but it must get the normalized static +`SurfaceToolContract` from `manifest.surfaceToolContracts` through +`getAgentManifestSurfaceToolContract(manifest, AGENT_SURFACE_ID_PULSE_MCP)` +and format it with `getAgentSurfaceToolPosturePresentation`; +external-adapter request/response filtering, including the `subscribe_events` +streaming exception and any capability omitted from the published Pulse MCP +surface allowlist, must be owned by the backend projection in +`ProjectManifestSurfaceToolContracts`; shared capability reads must use +`ResolveManifestSurfaceToolContract` and `ManifestSurfaceToolCapabilities` so +the normalized surface contract and the resolved `tools` affordance gate are +applied once. MCP initialize must also advertise the `tools` capability and +name "offered tools" in operating instructions only when that same published +surface tool contract resolves. MCP prompt support must similarly enter through +`MCPManifestSurfacePromptProjectionSupported`, which combines the target +surface's `prompts` affordance with the manifest-owned workflow prompt +catalogue before initialize or `prompts/list` advertises prompts, and through +`GetMCPPromptFromManifestSurface` before `prompts/get` renders any prompt. The +global `pulse_operations_loop` prompt is part of that same shared catalogue, +not a native Assistant-only starter: it must appear in Assistant and MCP only +when the manifest can support the full Patrol-to-Assistant-to-governed-action +flow, and it must guide contextual explanation, plan-first governed action, +approval or rejection only when the returned policy requires a decision, +policy-allowed execution, post-action verification, and finding resolution from +the same rendered prompt text. The rendered prompt must orient +first through `get_patrol_control_status` and re-read that status, resource +context, and findings before treating an outcome as verified. Successful render activity for +that starter is activation evidence for the Patrol issue-handling journey, not +proof of contextual collaboration, governed action execution, verification, or +finding resolution. +Native Assistant callers may request a preferred workflow prompt through safe +browser context, but the drawer must resolve that request against the +manifest-owned workflow prompt catalogue and render it through +`/api/ai/workflow-prompts/render`; it must not inline prompt templates or +silently append to an existing composer draft. Preferred workflow prompt +requests are one-shot browser seeds and must be cleared with the scoped handoff +payload after the first successful send. +The MCP `tools/call` execution +helper must +accept the manifest and surface id, apply the same affordance and surface-tool +allowlist used by `tools/list`, and only then delegate to neutral capability +HTTP execution; it must not accept a caller-supplied raw capability slice as the +MCP execution boundary. Frontend consumers may normalize a published +`surfaceToolContracts` entry for presentation, but must not infer MCP tools +from raw manifest capabilities when that entry is missing, and must not add a +Pulse-MCP-specific frontend helper alias around the generic surface resolver. +The generated Pulse MCP README follows the same rule: its surface-contract +block must be rendered by `MCPSurfaceContractMarkdown` from +`Manifest.SurfaceContract`, alongside the manifest-derived scope, tool, prompt, +and error-code inventories. +The public Pulse Intelligence overview follows the public-docs version of that +rule: the marked overview block in `docs/AI.md` must be rendered by +`PulseIntelligenceOverviewMarkdown` from the same manifest-owned +`SurfaceContract`, while surrounding product detail may remain human-authored. +The External agents settings panel follows the browser-facing version of +that same rule: its Pulse Intelligence Core, Patrol, Assistant, and MCP surface +summary must be projected from `/api/agent/capabilities.surfaceContract` through +`frontend-modern/src/api/agentCapabilities.ts`, not maintained as panel-local +copy; surface affordance badges are part of that same manifest projection. Its +capability-publication wording must describe published manifest-owned surface +contracts, not raw backend capability rows, so the panel does not imply that +adding a raw capability automatically exposes it to Pulse MCP. +Its external-agent recovery summary must also be manifest-backed: stable +failure-code chips or summaries may be rendered for setup ergonomics, but they +must derive from capability `errorCodes` through the shared frontend manifest +client rather than a panel-local recovery-code list. +External-agent token setup copy in that panel must route operators back to the +API Access token creation surface and the manifest-derived `Pulse Intelligence +agent` preset instead of carrying MCP-local token minting logic or a +component-local required-scope list. + ## Extension Points +Pulse Intelligence presents Patrol as the primary first-party operations +surface. Pulse Assistant is the in-app contextual explanation, approval-card, +governed-action, verification, and handoff access path over Patrol work. +`cmd/pulse-mcp/` is the external-agent adapter for MCP-speaking clients. The +adapter must stay thin: it fetches `/api/agent/capabilities`, projects that +canonical manifest as MCP tools, and forwards calls to the declared API routes. +The shared `MCPManifestToolServer` must therefore consume the manifest as a +unit, not only a detached capability slice, so initialize instructions, tool +projection, resources, prompts, notifications, and future manifest-backed +surface metadata remain coupled to the same discovery document. It must resolve +the target surface's affordance contract before initialize advertisement and +before tool, resource, and prompt handlers run; raw manifest capabilities, +workflow prompts, or context routes are availability inputs only after the +surface contract allows that affordance. +When an adapter renders a manifest prompt locally, it may notify Pulse through +the authenticated `POST /api/agent/workflow-prompt-activity` marker endpoint +only after the prompt render succeeds. That marker route accepts manifest-owned +prompt names only, requires the token to cover `monitoring:read`, normalizes +`X-Pulse-Agent-Surface: pulse_mcp` into the Pulse MCP surface, and records no +prompt arguments, rendered text, resources, findings, token identity, or +request payload. +`scripts/generate-pulse-intelligence-docs.go` is part of that coupling: it +must update the MCP README's Pulse Intelligence surface contract and the public +`docs/AI.md` overview from the manifest instead of leaving hand-written +Core/Patrol/Assistant/MCP relationship prose beside generated inventories. +Public AI docs may describe anonymous outbound telemetry only as counts, +feature flags, and coarse Patrol control and governed Pulse Intelligence +operations adoption flags and counters. +They must keep prompts, chat messages, command text, action output, token +values, resource identifiers, and hostnames explicitly outside that telemetry +scope. +Governed-operation workflow starter counts may appear in that outbound telemetry +only as 30-day aggregate counts for the canonical `pulse_operations_loop` +prompt, split by total, native Assistant, first-party Patrol, and Pulse MCP surfaces. Those counts +may make `pulse_intelligence_loop_active_30d` true because the operator entered +the guided journey, but they must not satisfy contextual-collaboration, +complete-loop, approved-execution, or resolved-loop telemetry by themselves. +Assistant cost-ledger events may carry only the coarse `context_scope` +classifier for product-originated finding, resource, handoff, action, or +structured-mention turns; telemetry may aggregate that classifier into +governed-context Assistant counts, but it must not export prompts, session IDs, +resource IDs, finding IDs, command text, or action payloads. +Assistant cost-ledger events may also carry only a count-only +`tool_call_count` for accepted, model-selected governed tool calls within the +turn. Telemetry may aggregate that number into Assistant governed-tool +collaboration counts, but it must not export tool names, tool arguments, tool +results, provider call IDs, transcript content, resource IDs, finding IDs, +command text, action payloads, or session IDs. +The Pulse MCP README and the in-app Agent integrations panel must present that +adapter as one reusable runtime contract with client-native config wrappers: +OpenCode uses a top-level `mcp` object in `opencode.json` / `opencode.jsonc`, +while Claude-style clients use `mcpServers`. The shared command, base URL, and +token environment variable remain the Pulse Intelligence contract; client file +formats are wrappers around that contract, not separate Pulse Intelligence +surfaces. In the in-app Pulse Intelligence settings panel, those copied wrapper +snippets are on-demand setup detail; they must not occupy the default +external-agent read above the Patrol-autonomy and scoped-token hierarchy. The +Security API Access page may mint the scoped token, but it must not become the +external-agent setup page. +Full-surface token scope guidance must come from the canonical manifest's +manifest-owned `requiredScopes` set through shared +`agentcapabilities.ManifestRequiredScopeList` / +`ManifestRequiredScopeMarkdownList` helpers rather than Assistant-, UI-, or +adapter-local hardcoded or recomputed scope copy. +The native Assistant runtime prompt, lifecycle logs, and readiness messages +must identify that first-party surface as Pulse Assistant rather than the +legacy generic `Pulse AI` persona; the shared engine/core vocabulary remains +Pulse Intelligence. +The canonical manifest declaration, manifest wire shape, action-mode enum, +registry tool-name vocabulary, structured registry tool schema, provider projection helpers, JSON Schema +object-envelope helpers, provider tool/call/result shapes, provider-call +projection helper, provider tool-name catalog exact/prefix matching, +provider tool-call artifact detection and streaming tool-name prefix holding, +provider tool-call input normalization that clones caller +maps and nested JSON-like argument values before initializing empty input +objects, provider-emitted streamed tool-input parsing, final provider +tool-input raw fallback handling, and native Assistant question input parsing, +control-level vocabulary, control-tool availability +predicate, tool-governance defaulting, disabled-control guidance, capability +lookup and typed lookup-error helpers, external-tool +projection helpers, and shared agent HTTP substrate, including named capability +execution, live in +`internal/agentcapabilities`; MCP JSON-RPC envelopes, method constants, +request decoding, line-delimited stdio request serving, notification response +policy, stable JSON-RPC encoding, manifest-backed tool-server semantics, +tool-server method dispatch, initialize instruction/tool-call/resource/prompt payloads, +tool-server initialize result construction, notification methods, MCP +`tools/call` raw params decode, MCP resource URI projection, context-backed +`resources/list` / `resources/read` projection, and manifest-backed +`prompts/list` / `prompts/get` projection live at the MCP adapter edge. +Native Assistant registry declarations are part of that same identity contract: +every registry `Tool.Definition.Name` for Pulse Intelligence tools, including +the read-only `pulse_summarize` reporting synthesis tool and Patrol runtime +tools, must consume `internal/agentcapabilities/tool_names.go` constants rather +than owning string literals in `internal/ai/tools`. +Shared Pulse Intelligence workflow prompt definitions, manifest-owned +`workflowPrompts` catalogue selection, prompt display labels, MCP prompt title +projection, presentation kind hints, prompt argument validation, and prompt +rendering rules live in the neutral workflow-prompt core so native Assistant +starters, frontend manifest consumers, and external MCP prompts cannot drift. +When `workflowPrompts` is present on a manifest it is authoritative; capability +projection is compatibility plumbing for older manifests, not a second prompt +catalogue. The native Assistant starter row must therefore derive starter +availability from `workflowPrompts` and the attached Assistant context, then call +the AI/API-owned render route before inserting any prompt text into the composer; +it must not copy prompt labels, prompt bodies, or MCP `prompts/get` rendering +logic into frontend state. MCP prompt list/get helpers must likewise accept the +manifest-owned workflow prompt catalogue through `ManifestPulseWorkflowPrompts`, +`ProjectMCPWorkflowPrompts`, `GetMCPPromptFromManifestSurface`, +`GetMCPPromptFromManifest`, and +`BuildMCPPromptFromManifest`; they must not expose raw-capability `ProjectMCPPrompts`, +`GetMCPPrompt`, or `BuildMCPPrompt` entrypoints that can re-create a second MCP +prompt catalogue. Shared tool-call +parameter normalization and validation live in the neutral tool-call core: tool +names are trimmed and required, argument maps +are cloned deeply enough to detach nested JSON-like `body` maps/slices and +initialized, and malformed or semantically invalid tool-call params must fail +before capability lookup so native Assistant execution, the legacy text +projection, and external MCP adapters cannot drift on empty-name, nil-argument, +or caller-owned nested argument handling. Shared capability tool HTTP execution +and direct execution output/error mapping live in the neutral tool-execution +core, while result wrapping, result-text extraction, result interpretation, and +the canonical `HTTPCallResponse` to shared tool-result mapping live in the +neutral tool-result core. JSON object result bodies must also project into the +same shared structuredContent envelope while preserving the serialized text +content block for compatibility, so MCP clients and native Assistant paths see +one canonical result contract. Native Assistant direct execution paths must use that +shared text/marker interpretation and output/error projection instead of +flattening tool-result content blocks or branching on `isError`, approval, or +policy outcomes locally, so in-app execution and external MCP adapters +interpret the shared result envelope the same way. MCP `tools/call` decoding is +adapter-edge protocol work, but execution authorization must still enter the +neutral tool-execution core only after the shared manifest surface contract has +selected the allowed request/response capabilities. Patrol-only Assistant +registry tools that return JSON must build those responses through the shared +`agentcapabilities` JSON result constructors instead of manually marshaling JSON +into text-only results, so `structuredContent` remains available to both native +Assistant and external-agent result consumers. Patrol verification +tool-call records that decide success, failure, +approval-required, or policy-blocked outcomes must also branch through the +shared tool result interpretation rather than treating `isError` alone as the +success boundary. Reference clients that need a successful raw response body must also +use the shared request/response capability body helper so named capability +lookup, path/body projection, API-token headers, and stable non-2xx error +formatting stay in one place. The +agent SSE subscription transport, record parser, actionable-record filter, and +SSE-to-MCP notification bridge also live there so MCP notification bridges and +reference probes consume the same `Accept: text/event-stream`, status handling, +`event:` / `data:` framing rules, transport-event filtering, and JSON-RPC +notification projection. +The legacy +approved-action text projection for Pulse tool calls is also parsed there into +the shared `ToolCallParams` shape, including the internal approved-action +argument key/helper that carries a granted approval into replayed tool execution, +the `current_resource` handle plus governed aliases, and the recursive +tool-argument detector that blocks unresolved attached-resource placeholders +before visible execution; text invocations are then normalized and validated +through the same shared parameter contract, so +Assistant approval execution and MCP-compatible adapters cannot drift on tool +name, `default_api:` prefix, quoted argument handling, placeholder target +semantics, empty names, nil +arguments, or pre-approved action replay semantics. Structured tool +response envelopes, tool error-code vocabulary, the structured tool-result +error-code parser, and the `verification.ok` evidence parser also live there +so Assistant blocked/failed tool outcomes, provider call params, agent-facing +tool failure contracts, FSM block/recovery codes, recovery tracking, and +write-tool self-verification semantics cannot drift. The legacy-compatible Assistant tool markers +for approval-required and policy-blocked outcomes also live there, including +the stable `APPROVAL_REQUIRED:` / `POLICY_BLOCKED:` prefixes, payload `type` +values, formatter helpers, parser helpers, and the typed approval-required +payload contract for plan, context-confidence, preflight, target, risk, and +description data. Tool families may own the resource-specific payload fields +they add to those markers, but they must not redeclare the marker envelope, +rebuild anonymous approval payload readers, branch on local prefix strings, or +hide shared marker parsing behind Assistant-local parser wrappers. Legacy +service approval-marker handling may keep compatibility fallback behavior for +old marker text, but the canonical decision path must call the shared marker +parser directly at the branch site. Tool execution must not drop the returned +argument map when carrying a granted approval into replayed tool execution. +Native Assistant tool protocol aliases must expose only the shared neutral +registry shapes the Assistant actually executes (`Tool`, `ToolCallParams`, +`ToolResult`, `ToolContent`, and structured tool-response envelopes). JSON-RPC, +MCP initialize/list/resource/prompt payloads, and other MCP wire aliases remain +at the external adapter edge in `internal/agentcapabilities/mcp.go`, not in +`internal/ai/tools/protocol.go`. Chat transcript and API-facing tool-result +shapes must alias the shared provider-call contract while the richer in-app +Assistant transcript projects to that shared shape through an explicit helper. +Provider-result context projection also belongs to `internal/agentcapabilities`: +native Assistant, the legacy AI service loop, external adapters, and future +Pulse Intelligence surfaces must build full transcript results and model-context +results through the shared projection so transcript preservation, error flags, +and model-result truncation cannot drift into surface-local pairs. +API handlers, Assistant tool governance, `cmd/pulse-mcp`, and the in-repo probe +consume that shared contract so agent projections cannot drift into local +structs, local structured schema types, local provider schema projection, local +schema wrappers, local request/response filters, local capability maps, local +path placeholder parsing, local argument-to-body shaping, local manifest +fetchers, local API-token header spelling, local stable error-envelope +formatting, local named capability execution, local tool-governance defaulting, +local disabled-control guidance, local MCP manifest-backed +tool-server handlers, local MCP method dispatch, local MCP request decoding or +line-framing loops, local JSON-RPC encoders, local notification response +checks, local MCP initialize builders, local +HTTP-to-MCP result wrapping, local MCP result interpretation, +local SSE record scanners, local SSE-to-MCP notification bridges, local +tool-result maps, or local tool-description builders. Path placeholders from the manifest and top-level +tool arguments must be projected through the shared +`agentcapabilities.ProjectCapabilityCall` helper before dispatch, so path +values are percent-encoded single segments and non-placeholder write arguments +become the JSON request body when no nested `body` object is provided. +Internal tool-call metadata such as approved-action grants must stay on the +shared `agentcapabilities` argument helper path and be stripped by the shared +projection before any public manifest-backed HTTP body is built. +Canonical IDs containing `:`, `/`, spaces, or other reserved characters cannot +alter the route shape. +The Assistant and MCP surfaces must also rely on the router-owned manifest +route/scope proof: every canonical capability is projected through the shared +call helper into a concrete request, then exercised against the API router with +a token that lacks the advertised scope so drift between manifest metadata, +agent-facing placeholders, and real authorization fails before either surface +ships a mismatched tool. +Native Assistant provider seams are part of the Pulse Assistant surface, not +the external MCP adapter. Chat service aliases, API handler setter signatures, +router dependency wiring, direct in-app tool execution, and findings-store +adapters plus the broader native tool adapter family must use Assistant/Pulse +Intelligence terminology; `MCP` names are reserved for the protocol adapter, +MCP-compatible action-contract interface, wire envelopes, and result +interpretation contracts. +The Pulse Intelligence event vocabulary is part of that same shared +`internal/agentcapabilities` boundary. API SSE producers, the +`subscribe_events` manifest description, MCP notification advertising, MCP +transport-event filtering, MCP notification method projection, shared SSE +record parsing, shared SSE-to-MCP notification bridging, and reference probes +must consume the shared event kind constants/helpers instead of maintaining local copies of +`finding.created`, `approval.pending`, `action.completed`, `stream.connected`, +or `heartbeat`. +MCP `tools/list` must use the shared +`agentcapabilities.ProjectManifestSurfaceTools` projection for the Pulse MCP +surface. The manifest-owned `surfaceToolContracts` field is the allowlist for +which request/response tools Pulse MCP may advertise or execute; raw +`Manifest.Capabilities` remains the metadata and execution source, not the +surface availability decision. MCP initialize capability advertisement, +operating-instruction affordance copy, `tools/list`, `tools/call`, +`resources/list`, `resources/read`, `prompts/list`, and `prompts/get` must also +pass through the manifest-owned surface affordance contract: the surface must +allow tools, resources, prompts, and capability metadata before raw manifest +capability or prompt presence can expose them to an external client. The shared +`agentcapabilities.ProjectTools` +helper still owns the full request/response tool shape, including +manifest-owned capability metadata (category, method/path, scope, action mode, +approval policy, request/response shape, stable error codes, input schema, and +structured output schema). That metadata must be available both in model-facing +description prose and in the standard MCP `_meta["pulse.capability"]` object +for clients that need structured route/scope/governance facts, so external-agent +clients see the same governed API contract without a second MCP-specific tool +registry, projection loop, allowlist, or prose table. Action mode (`read`, +`mixed`, `write`) and approval policy (`scope_only`, `action_plan`) are +manifest-owned governance fields; MCP must not infer or override them from HTTP +method or local tool names. Manifest-backed capability lookup and `tools/list` +projection must also detach raw `inputSchema`, raw `outputSchema`, structured +metadata maps, and error-code slices through `agentcapabilities.CloneCapability`, +`CloneCapabilities`, and `CloneRawMessage`, so external-agent adapters cannot +mutate the manifest-backed contract while serializing or post-processing one +projected tool list. +The manifest-owned `get_patrol_control_status` structured output schema is +part of the same AI runtime contract. Its four-step operator rollup is a +content-free stage projection for Assistant and MCP callers: governance step +counts represent pending approvals until an approved or rejected decision +exists, then represent decision evidence, while verification step counts +represent verified outcomes or terminal rejected decisions. Optional MCP +readiness remains a separate `externalAgentReady` capability signal, not an +operator step. AI runtime surfaces must consume that manifest schema and backend +projection instead of inventing chat-local stage-count rules. The schema's +Patrol control starter count includes native Patrol, current paid Patrol +control, and legacy Pro activation handoff starts; its +completed-loop and resolved-loop counts are aggregate orientation fields for +native Assistant and MCP-facing orchestration only. Legacy +`patrolAutonomy*` and `proActivation*` completed-loop, resolved-loop, and +value-state fields are aliases for the Patrol control values; the legacy Pro +activation starter field remains an entry-point-specific count. All of +those fields must remain free of prompt names, finding IDs, action IDs, +resource names, actors, request bodies, commands, or model output. +Resolved-loop semantics remain approved-and-verified only. +`cmd/pulse-mcp` startup guidance and the generated MCP README token-scope +block must consume the manifest-owned `requiredScopes` summary through +`agentcapabilities.ManifestRequiredScopeList` / +`agentcapabilities.ManifestRequiredScopeMarkdownList`. MCP README tool and +capability-specific error-code inventories are generated through +`scripts/generate-pulse-intelligence-docs.go` from the manifest-owned Pulse MCP +surface contract via `agentcapabilities.MCPToolCapabilityInventoryMarkdown` and +`agentcapabilities.MCPErrorCodeInventoryMarkdown`. Workflow-prompt inventory is +generated through `agentcapabilities.MCPPromptInventoryMarkdown` from the same +`MCPManifestSurfacePromptProjectionSupported` gate and +`ManifestPulseWorkflowPrompts` / `ProjectMCPWorkflowPrompts` projection used by +`prompts/list`, including manifest labels as prompt titles. README tool lists, +prompt lists, scope lists, and capability-specific error-code lists must not be +hand-maintained snapshots. MCP +`tools/call` must use the same shared +`agentcapabilities.ManifestSurfaceToolCapabilities` resolver as `tools/list`; +streaming capabilities such as `subscribe_events`, and raw manifest +capabilities omitted from the Pulse MCP surface contract, remain unavailable to +request/response tool calls even if a client sends their name manually. Adding +a raw capability to `Manifest.Capabilities` is not enough to publish an MCP +tool; the Pulse MCP `surfaceToolContracts` allowlist must opt it into that +external-agent surface. Publishing the Patrol work prompt also requires the +surface contract to include `get_patrol_control_status`, so MCP clients can +read the same content-safe Patrol work stage before requesting fleet, finding, +resource, approval, action, or resolution detail. The legacy +`get_operations_loop_status` name may remain accepted only at compatibility +edges. +MCP `resources/list` and `resources/read` are also shared manifest-backed +projections: `resources/list` must call the canonical `get_fleet_context` +capability, `resources/read` must call `get_resource_context`, and the +`pulse://resource/` URI shape, resource content MIME type, and +resource read parameter validation live in `internal/agentcapabilities` rather +than in `cmd/pulse-mcp` or an MCP-only inventory registry. They must enter +through `ManifestSurfaceResourceCapabilities`, +`ListMCPManifestSurfaceResourcesHTTP`, and +`ReadMCPManifestSurfaceResourceHTTP`, not raw-capability +`ListMCPResourcesHTTP` or `ReadMCPResourceHTTP` helpers. The MCP resource +methods remain disabled when the target surface affordance contract disables +resources, even if those context capabilities exist in the manifest. +Operator-state writes, governed finding lifecycle tools, and action +plan/decision/execute tools must also carry manifest-owned `inputSchema` +definitions for their exact arguments so external-agent clients receive typed +fields and enums from the canonical manifest instead of an MCP-local free-form +body convention. When the manifest has no authored schema, MCP may derive a +permissive fallback through the shared `agentcapabilities.ToolInputSchema` +helper, but it must not hand-roll a second JSON Schema envelope. +Governed finding lifecycle tools must return the agent-stable error envelope +from `internal/agentcapabilities/errors.go` for branchable failures +(`invalid_finding_request`, `finding_not_found`, +`finding_action_not_allowed`, `patrol_unavailable`) and declare those codes in +the canonical manifest; UI callers may still use the same HTTP routes, but the +wire contract is owned by the Pulse Intelligence agent surface once the route +is advertised there. The exported `agentcapabilities.AgentErrCode*` constants +are the source of those values for both manifest declarations and +`internal/api/` handler emissions; local string literals in either layer are +contract drift. Manifest `scope` values are auth-owned vocabulary from +`pkg/auth` and must be declared through manifest-local aliases to those +constants, not repeated as string literals, so MCP tools and direct HTTP agents +advertise the same scopes the API authorization layer enforces. Patrol finding +capabilities (`list_findings`, `acknowledge_finding`, `snooze_finding`, +`dismiss_finding`, and `resolve_finding`) are part of the AI runtime route +surface and therefore advertise `ai:execute`; they must not be downgraded to +monitoring scopes in MCP docs or manifest projections while the router gates +those HTTP routes through AI execution authorization. +Do not add MCP-only business logic or a second tool registry here. If a new +operation should be available to external agents, add it to the canonical API +capability manifest and make Assistant consume the same governed backend +contract when the in-app UX needs it. +Assistant prompt governance follows the same rule: live turns derive tool +mode, typed approval policy (`scope_only` or `action_plan`), approval summary, +and tool summary text from the shared `agentcapabilities.ToolGovernanceDescriptor` +shape returned by `PulseToolExecutor.ListToolGovernance`; descriptor defaults +must be constructed through `agentcapabilities.NewToolGovernanceDescriptor`, +and fallback prompt +text must call the registry-owned +`tools.CanonicalToolGovernanceForSurface` helper with the explicit requesting +Pulse Intelligence surface rather than maintaining a chat-local copy of tool names, governance +descriptor fields, policy vocabulary, or governance prose. +Assistant-only structured interaction tools such as `pulse_question` must also +be projected through the shared +`agentcapabilities.BuildAssistantToolGovernancePromptSection` and +`agentcapabilities.AssistantGovernanceOfferedToolNames` helpers rather than by +hand-building prompt-policy lines or offered-tool filters in chat-local code; +those tools remain native Assistant interaction boundaries, not MCP manifest +capabilities. +Direct Assistant registry execution is also part of the shared Pulse +Intelligence tool-call contract: `ToolRegistry.Execute` must normalize and +validate tool names and arguments through `agentcapabilities.ToolCallParams` +and use the shared invalid-params, unknown-tool, and control-disabled tool +result helpers before lookup or handler dispatch, so in-app direct execution +cannot drift from external MCP `tools/call` on trimmed names, blank-name +rejection, nil-argument initialization, deep caller-argument copying, or +standard entrypoint failure envelopes. +Chat-service direct execution paths such as autonomous command execution and +approved-fix replay must route through one Assistant registry execution helper +before calling `agentcapabilities.InterpretDirectToolExecution`, so +surface-specific failure wording does not fork executor lookup, registry +dispatch, marker interpretation, or shared result handling. MCP-named +direct-execution symbols are not part of the shared adapter contract; native +Assistant code and MCP adapters must use the neutral direct tool execution +helper. +Cross-repo auto-fix and Patrol-approved action binders must resolve approved +tool execution through `AIAutoFixHandlerDeps.ResolveApprovedAssistantToolExecutor`; +that resolver returns the native `ApprovedAssistantToolExecutor` only, so +enterprise code cannot recreate MCP-named Assistant execution selection. The +current public runtime must populate `AssistantToolExecutor` only; MCP remains +an external adapter boundary and is not an approved-fix execution dependency. +Assistant telemetry that records recoverable policy-block attempts must also +use the shared `agentcapabilities` tool-response error-code constants rather +than local string literals, so metrics labels, structured tool responses, +Assistant recovery tracking, and MCP-facing agent error vocabulary stay +aligned. +Assistant provider tool schemas are also registry-owned: chat must obtain the +runtime provider tool surface from `PulseToolExecutor.AssistantProviderTools` +so availability filtering, registry-owned governance descriptors, and +Assistant-native interaction tools are composed at one executor-owned boundary +instead of being paired manually in chat paths. That executor projection must +project registered `tools.Tool.InputSchema` values through the shared +`agentcapabilities.ProjectProviderTools` provider-schema helper and the +manifest-affordance-gated +`agentcapabilities.ProjectPulseAssistantProviderTools` provider-surface helper, +and model-selected tool calls must flow through +`agentcapabilities.ProjectProviderToolCallToToolCall` and the shared +`agentcapabilities.NormalizeProviderToolCallForExecution` / +`NormalizeProviderToolCallsForExecution` execution normalizers, not local schema +maps, compatibility aliases, raw provider call fields, chat-local provider +projection wrappers, or MCP-shaped provider-projection wrappers. +MCP-shaped provider projection names remain protocol compatibility aliases in +`internal/agentcapabilities/mcp.go`; they must not be implemented in the neutral +provider schema boundary or imported by native Assistant runtime paths. +Shared projected-tool behavior hints are the same class of boundary: the +neutral projection owns `ToolBehaviorHints`, while any `MCPToolAnnotations` +name is an alias in the MCP wire edge only. +Assistant-only provider tools are still shared Pulse Intelligence declarations: +the native `pulse_question` clarification tool must be included in provider +tool lists through `agentcapabilities.ProjectPulseAssistantProviderTools` and +`agentcapabilities.AssistantNativeProviderTools`, its declaration must come +from `agentcapabilities.NewPulseQuestionProviderTool`, its JSON Schema must +remain owned by `agentcapabilities.PulseQuestionProviderInputSchema`, its +question type/defaulting vocabulary must use +`agentcapabilities.PulseQuestionToolTypeValues` and +`agentcapabilities.NormalizePulseQuestionToolType`, its emitted provider input +must parse through `agentcapabilities.ParsePulseQuestionToolInput`, and +allowlists that recognize leaked provider tool calls must consume +`agentcapabilities.NewAssistantProviderToolNameCatalog` instead of hard-coding +Assistant-native names or rebuilding provider-tool name sets locally. Provider +tool-call artifact detection and streaming partial-tool-name holding must use +`agentcapabilities.ProviderToolCallArtifactIndex` and +`agentcapabilities.SplitTrailingProviderToolNamePrefix` with that shared catalog, +so Assistant and future adapter boundaries do not fork DSML/XML/pipe/JSON/function +leak semantics. Chat may own waiting for user answers, UI event +emission, and FSM user-input behavior, but it must not carry a chat-local +provider schema, description, tool-list append rule, prompt-filter rule, parser, +tool identity, or provider artifact detector for that interaction. +That schema boundary is also a copy boundary: registry tool definitions must +normalize through `agentcapabilities.Tool.NormalizeCollections`, and +`ToolRegistry.Register` / `ToolRegistry.ListTools` must not expose caller-owned +or registry-owned `InputSchema` maps, required slices, enum slices, provider +input-schema maps, or JSON-schema default containers for later mutation by an +Assistant, provider, or external-agent surface. Direct provider schema +projection helpers must normalize through that same copy boundary before +emitting provider JSON so JSON-schema default containers cannot alias registry +definitions. +Provider tool-call normalization must also stay shared: provider responses, +Assistant transcript storage, live Assistant execution, and API adapter +projections must get independent input maps, nested JSON-like argument values, +trimmed executable tool names, initialized empty argument maps, and raw provider +thought-signature payload bytes from +`agentcapabilities.ProviderToolCall.NormalizeCollections` plus the shared +execution normalizers so a later mutation in one surface cannot rewrite another +surface's tool arguments or provider reasoning-continuity metadata. +Assistant transcript normalization is part of that copy boundary: +`chat.Message.NormalizeCollections` and `chat.ToolCall.NormalizeCollections` +must detach tool-call slices, nested tool-call input maps/slices, provider +thought signatures, success pointers, and tool-result pointers before stored +messages are reused across session storage, provider-continuation, browser/API, +or orchestrator adapter surfaces. +Provider-facing tool results follow the same shared boundary: Assistant +tool execution must project shared tool-call results through +`agentcapabilities.NewProviderToolResultFromToolResult` and build transcript or +model-context result messages through `agentcapabilities.NewProviderToolResult` +so result text flattening and `is_error` semantics stay aligned with +external-agent adapters. MCP-named provider-result projection helpers are not +part of the adapter contract; runtime paths that need the plain text from a +shared tool result must call `agentcapabilities.ToolResultText` at the execution +site instead of keeping Assistant-, Patrol-, or MCP-local result-text wrappers. +Runtime paths that convert chat transcripts, repair orphan tool calls, or run +legacy service tool loops must not assemble `providers.ToolResult` or +`chat.ToolResult` structs locally, and must not hide shared provider-result +construction behind Assistant-local pointer wrappers. +Native Assistant registry execution and external MCP method dispatch must also +normalize shared `ToolResult` values through +`agentcapabilities.ToolResult.NormalizeCollections` before returning them, so +handler-owned content slices and nil content collections cannot leak across +Assistant, provider, or MCP response boundaries. MCP may retain only actual +`MCPToolResult` / `MCPContent` protocol wire aliases; result constructors, +structured tool-response projection, JSON object and array-wrapper +structuredContent projection, text extraction, and interpretation stay neutral +shared helpers rather than MCP-named wrappers. +Assistant runtime settings and tool exposure must derive +read-only/controlled/autonomous semantics from +`agentcapabilities.ControlLevel` and +`agentcapabilities.ControlLevelAllowsControlTools`; future MCP adapters may +alias the same values, but must not carry separate control-tool availability +rules, disabled-control guidance, or tool-governance defaulting. + Guest-family AI runtime code paths (VMs and LXC system containers) share generic helpers instead of per-family copies. Patrol guest intelligence gathers both families through `gatherGuestIntelligenceFromViews` in @@ -70,10 +930,15 @@ driven by `fileMutationSpec` (`internal/ai/tools/tools_file.go`), and namespaced kubectl actions share `executeKubernetesResourceAction` driven by `kubernetesResourceAction` (`internal/ai/tools/tools_kubernetes.go`) — new file or kubectl actions add a spec, keeping approval-command text stable, -instead of duplicating the approval/audit pipeline. Anthropic message -conversion is shared by the API-key and OAuth clients through -`convertMessagesToAnthropic` (`internal/ai/providers/anthropic.go`), and AI -memory history files load through the generic `loadMemoryHistory` +instead of duplicating the approval/audit pipeline. OpenAI-compatible message +conversion for both non-streaming and streaming turns is shared through +`convertMessagesToOpenAI` (`internal/ai/providers/openai.go`), Anthropic message +conversion for both non-streaming and streaming turns is shared by the API-key +and OAuth clients through `convertMessagesToAnthropic` +(`internal/ai/providers/anthropic.go`), Gemini message conversion for both +non-streaming and streaming turns is shared through `convertMessagesToGemini` +(`internal/ai/providers/gemini.go`), and AI memory history files load through +the generic `loadMemoryHistory` (`internal/ai/memory/paths.go`). `Finding`/`findingJSON` and `UnifiedFinding`/`unifiedFindingJSON` are deliberate marshal-mirror twins — do not merge them; the mirror invariants are enforced by @@ -83,7 +948,14 @@ the transport side: `orchestratorChatAdapter.GetMessages` (`internal/api/ai_handlers.go`) and `chatServiceAdapter.GetMessages` (`internal/api/chat_service_adapter.go`) convert the same chat-service messages onto deliberately separate output contracts; do not merge them, and -keep their field mapping aligned — +keep their field mapping aligned. Orchestrator-facing tool-call and +tool-result payloads must project through +`aicontracts.OrchestratorToolCallInfoFromProvider`, +`OrchestratorToolCallInfo.ProviderToolCall`, +`aicontracts.OrchestratorToolResultInfoFromProvider`, and +`OrchestratorToolResultInfo.ProviderToolResult` so the public investigation +handoff contract inherits the same `agentcapabilities` input-map, +thought-signature, and provider-result normalization as Assistant and MCP — `TestOrchestratorAndChatAdaptersMapTheSameMessageFields` enforces it. Assistant frontend presentation changes under @@ -158,30 +1030,30 @@ deriving an older display status from `workflowStatusHistory`. `ResourceRoutingScopeLocalOnly` (Restricted sensitivity — tagged secret/pii, PMG, k8s-secrets, ...) have their identifiers redacted and never leave the local trust boundary. Everything else (Internal and Sensitive) flows. - Both are enforced by the model-boundary sanitizer - `modelboundary.RequestSanitizerForModel(model, provider)` (`internal/ai/modelboundary`), - which returns nil for local (Ollama) models — local always receives full context. - UNIVERSAL backstop rule: EVERY code path that sends infrastructure-derived - content to an external model MUST install `RequestSanitizerForModel` before the - provider request — not only the interactive agentic loop. This explicitly - includes session compaction (`internal/ai/chat/session_compaction.go` - `SummarizeSession`), which sends the PERSISTED transcript (original user prompts - and tool outputs carry raw identifiers), and the shared service helper - `(*Service).requestSanitizerForModel` (`internal/ai/service.go`) used by - discovery analysis, the report and fleet narrators, quick analysis, and the - ExecuteAgentic paths. A new model-bound request path that skips the sanitizer is - a leak and is not permitted. (Static capability probes with no resource content, - e.g. the Patrol preflight self-test, are exempt only because their payload is - fixed and carries no identifiers.) - Redaction-placeholder hygiene: Pulse-authored model-bound directives (the - resource-context handoff instructions in `internal/ai/chat/service.go` and - `internal/ai/chat/plain_text_resource_context.go`) must NOT inject the literal - redaction placeholder (`unifiedresources.ResourcePolicyRedactedLabel`, - "redacted by policy") into the prompt — naming it makes the model echo it back - as if it were a resource name. Directives reference withheld labels neutrally - ("a withheld or placeholder label") and must instruct the model not to repeat a - withheld placeholder back to the user as the resource identity; the - `current_resource` handle remains the authoritative target. + Both are enforced by the model-boundary sanitizer + `modelboundary.RequestSanitizerForModel(model, provider)` (`internal/ai/modelboundary`), + which returns nil for local (Ollama) models — local always receives full context. + UNIVERSAL backstop rule: EVERY code path that sends infrastructure-derived + content to an external model MUST install `RequestSanitizerForModel` before the + provider request — not only the interactive agentic loop. This explicitly + includes session compaction (`internal/ai/chat/session_compaction.go` + `SummarizeSession`), which sends the PERSISTED transcript (original user prompts + and tool outputs carry raw identifiers), and the shared service helper + `(*Service).requestSanitizerForModel` (`internal/ai/service.go`) used by + discovery analysis, the report and fleet narrators, quick analysis, and the + ExecuteAgentic paths. A new model-bound request path that skips the sanitizer is + a leak and is not permitted. (Static capability probes with no resource content, + e.g. the Patrol preflight self-test, are exempt only because their payload is + fixed and carries no identifiers.) + Redaction-placeholder hygiene: Pulse-authored model-bound directives (the + resource-context handoff instructions in `internal/ai/chat/service.go` and + `internal/ai/chat/plain_text_resource_context.go`) must NOT inject the literal + redaction placeholder (`unifiedresources.ResourcePolicyRedactedLabel`, + "redacted by policy") into the prompt — naming it makes the model echo it back + as if it were a resource name. Directives reference withheld labels neutrally + ("a withheld or placeholder label") and must instruct the model not to repeat a + withheld placeholder back to the user as the resource identity; the + `current_resource` handle remains the authoritative target. 3. Add or change Pulse Assistant request flow through `internal/api/ai_handler.go`, `frontend-modern/src/api/ai.ts`, and `frontend-modern/src/api/aiChat.ts` Assistant session compaction is a runtime-backed session workflow, not a local waiting message, transcript-only UI action, or stubbed summarize @@ -369,17 +1241,17 @@ deriving an older display status from `workflowStatusHistory`. question controls live in the transcript. The referenced OpenCode source at commit `9ed17da55ab1f7360cc0e01075f763e27fa899e9` mutates active tool parts into terminal `success` or `failed` rows in - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx` - (`session.next.tool.success`, lines 328-348; - `session.next.tool.failed`, lines 350-371), and the processor marks - interrupted active tools terminal before completing the assistant message in - `packages/opencode/src/session/processor.ts` (lines 888-917). Pulse adapts - that by retaining completed tool rows and assistant text while removing - unresolved interactive rows from terminal browser transcript state. - Completed tool rows must preserve timing once a pending row resolves. The - referenced OpenCode source at fetched `origin/dev` commit - `effd27b23900720a53e965396ff1a105c1f7e9c8` carries tool identity and - state through `toolCommit` / `startTool` / `doneTool` in + `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx` + (`session.next.tool.success`, lines 328-348; + `session.next.tool.failed`, lines 350-371), and the processor marks + interrupted active tools terminal before completing the assistant message in + `packages/opencode/src/session/processor.ts` (lines 888-917). Pulse adapts + that by retaining completed tool rows and assistant text while removing + unresolved interactive rows from terminal browser transcript state. + Completed tool rows must preserve timing once a pending row resolves. The + referenced OpenCode source at fetched `origin/dev` commit + `effd27b23900720a53e965396ff1a105c1f7e9c8` carries tool identity and + state through `toolCommit` / `startTool` / `doneTool` in `packages/opencode/src/cli/cmd/run/session-data.ts` (lines 622-733) and formats elapsed tool state with `span` in `packages/opencode/src/cli/cmd/run/tool.ts` (lines 191-200). Pulse adapts @@ -417,16 +1289,16 @@ deriving an older display status from `workflowStatusHistory`. `pulse_read` or bare fallback labels such as `read` remain fallback-only debug/detail vocabulary. Live workflow activity is also active turn state, not disposable waiting - copy. The referenced OpenCode source at fetched `origin/dev` commit - `1025540fcc2a69609a0131a7168300205656d728` defines model, shell, step, - and tool lifecycle events in `packages/core/src/session/event.ts` (model - switched lines 62-70; shell started/ended lines 151-174; tool lifecycle - lines 340-392), projects them into durable message/part rows in - `packages/core/src/session/message-updater.ts` (step started lines - 189-210; tool called/progress/success/failed lines 269-335), and renders - model-switch plus tool rows in - `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx` - (model switch lines 120-122 and 262-274; tool rows lines 455-500). + copy. The referenced OpenCode source at fetched `origin/dev` commit + `1025540fcc2a69609a0131a7168300205656d728` defines model, shell, step, + and tool lifecycle events in `packages/core/src/session/event.ts` (model + switched lines 62-70; shell started/ended lines 151-174; tool lifecycle + lines 340-392), projects them into durable message/part rows in + `packages/core/src/session/message-updater.ts` (step started lines + 189-210; tool called/progress/success/failed lines 269-335), and renders + model-switch plus tool rows in + `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx` + (model switch lines 120-122 and 262-274; tool rows lines 455-500). Pulse adapts that by keeping the latest browser `workflow_status` activity visible in the live stream event sequence across content, tool, approval, and question boundaries instead of dropping it the instant a richer row @@ -1180,10 +2052,10 @@ deriving an older display status from `workflowStatusHistory`. `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx`. Pulse's compact tool rows must follow that operator-language model: the row should summarize the actual governed action (`search "prowlarr"`, `list - active alerts`, `topology summary`, `Inspect devices on current resource`, +active alerts`, `topology summary`, `Inspect devices on current resource`, or exact governed write commands where the command itself is the operator decision) instead of exposing only internal action names such as `QUERY - search`, `exec`, provider function calls, or raw JSON; raw input and full +search`, `exec`, provider function calls, or raw JSON; raw input and full output stay available behind Details. The referenced OpenCode source at fetched `origin/dev` commit `06d7840d1d42c9815d2d2e45e7fa4090ca4e3577` renders Bash tool state through @@ -1205,7 +2077,7 @@ deriving an older display status from `workflowStatusHistory`. (`InlineTool`, lines 563-651; `Bash`, lines 701-735; `Read`, lines 758-770; `Grep`, lines 788-790). Pulse adapts that by using action-specific pending copy such as `Writing command...`, `Preparing - query...`, and `Reading storage...` before streamed tool arguments are +query...`, and `Reading storage...` before streamed tool arguments are parseable, then replacing that copy with the governed command/query/resource summary as soon as input arrives. The referenced OpenCode source at fetched `origin/dev` commit @@ -1614,6 +2486,12 @@ deriving an older display status from `workflowStatusHistory`. logged server-side or attached as redacted internal Patrol evidence where governed, but they must not be returned through the browser provider-test contract. + Anthropic runtime provider execution must be API-key backed. Legacy + Anthropic OAuth tokens may remain in encrypted settings only as cleanup + state; they must not make Anthropic configured, instantiate an OAuth-backed + provider, refresh tokens, or send model requests. OAuth start/exchange and + callback handling must fail closed, while disconnect may clear stored legacy + tokens. Patrol findings history transport must stay bounded when resolved findings are included: `/api/ai/patrol/findings?include_resolved=1` defaults to a 200-finding limit and caps explicit limits at 500, and the frontend Patrol @@ -1644,6 +2522,16 @@ deriving an older display status from `workflowStatusHistory`. remain route-distinct: if the configured chat override resolves to the same route as the effective default or the already selected session model, the drawer must not render a duplicate override action. + Assistant provider-readiness actions must send operators to the canonical + Pulse Intelligence > Provider & Models route + `/settings/pulse-intelligence/provider`; `/settings/system-ai` remains a + compatibility alias for old deep links rather than the href emitted by new + Assistant repair actions. + Assistant discovery-context hints must use the same product language as + Pulse Intelligence settings: the visible hint is `Discovery is off`, while + the body explains that enabling it gives Assistant real service, version, + and command context instead of generic guidance. The hint must not + reintroduce the old `Workload Discovery` label as a separate product concept. Assistant route and control chrome belongs with the prompt surface, not the drawer title row. The referenced OpenCode source at commit `9ed17da55ab1f7360cc0e01075f763e27fa899e9` @@ -1727,6 +2615,13 @@ deriving an older display status from `workflowStatusHistory`. edit, remove, promote, resume, and clear controls, but the primary headline must not imply the user's just-submitted follow-up disappeared behind a generic `Generating response` state. + Restored Patrol mode save-failure handoffs may preserve compatible + summary kinds such as `patrol_configuration_failure`, but Assistant drawer + titles, subjects, action labels, and safety notes must describe Patrol mode + rather than reviving generic configuration wording. Stable route markers, + telemetry fields, and API wire names may retain `patrol_control`; + user-facing Assistant, external-agent, generated MCP, and frontend copy must + call the operator's choice `Patrol mode`. 7. Keep AI chat presentation helpers aligned through `frontend-modern/src/components/AI/Chat/` and the shared `frontend-modern/src/utils/textPresentation.ts` 8. Keep assistant drawer context, session, and org-switch reset state aligned through the shared `frontend-modern/src/stores/aiChat.ts` boundary instead of letting `frontend-modern/src/App.tsx`, `frontend-modern/src/AppLayout.tsx`, or feature callers fork their own assistant shell state That shared drawer ownership also covers passive resource reads while the @@ -1754,25 +2649,52 @@ deriving an older display status from `workflowStatusHistory`. attached, the Assistant empty message state must also remain source-named and must not fall back to generic cluster/system starter prompts that compete with the attached briefing. - Structured Patrol run and Patrol configuration handoffs may render bounded, + Structured Patrol run and Patrol mode save-failure handoffs may render bounded, redacted diagnostic lines in the drawer when they are opened directly from - Patrol runtime/configuration surfaces, but the attached headline must remain - source-owned (`Patrol run attached`, `Patrol configuration ... attached`) + Patrol runtime/control surfaces, but the attached headline must remain + source-owned (`Patrol run attached`, `Patrol mode ... attached`) and the drawer must still exclude raw provider payloads, commands, and Patrol-authored remediation steps. Reloaded Assistant sessions may consume the backend-owned `handoff_summary` only as safe presentation state and a Patrol finding pointer; hidden model context, command payloads, preflight data, and action results stay backend-owned and must not be reconstructed in the browser. -9. Add or change public AI overview wording through `docs/AI.md`; it may +9. Add or change public Pulse Intelligence overview wording through `docs/AI.md`; it may describe Assistant and Patrol capabilities, but it must not revive legacy commercial shorthand such as `incident memory` as a current product promise. + The public overview must preserve the product architecture: Pulse + Intelligence Core owns canonical context, governed actions, safety gates, + approval state, action audit, and verification; Pulse Patrol is the primary + built-in operator on that core; its public summary must stay concise as + watching, investigating, acting within the chosen Patrol mode, verifying outcomes, + and recording what happened; and Pulse Assistant plus Pulse MCP are + contextual and external-agent access paths over the same governed + capabilities. + Public overview, safety, and onboarding copy must use the same visible + Patrol mode labels as the product (`Watch only`, `Ask before changes`, + `Auto-fix safe issues`, `Policy autopilot`) and must not reintroduce the + retired `Only watch`, `Fix safe issues`, or `Full control` labels as + customer-facing names. The public overview must also preserve the model-owned AI boundary: Pulse Assistant and Patrol provide governed context, tools, safety gates, approval state, and audit trails, while the configured LLM owns diagnosis, - prioritization, remediation reasoning, and tool choice. Public copy must not + prioritization, fix reasoning, and tool choice. Public copy must not present Pulse code as the intelligence engine, a prompt-keyword router, a - learned operational-meaning engine, or a deterministic finding author. + deterministic auto-remediation oracle, a provider replacement, a learned + operational-meaning engine, or a deterministic finding author. + The public overview must not carry a hand-maintained Assistant tool table. + Assistant tool inventory, action modes, approval policies, provider + declarations, and Patrol-only filtering are registry-owned in + `internal/ai/tools/` and projected through `internal/agentcapabilities/`; + external-agent inventory belongs to `/api/agent/capabilities` and + `pulse-mcp` `tools/list`. + External-agent setup metadata follows the same rule: Pulse MCP server name, + command, base URL flag/default, token environment variable, and supported + client config families are manifest-owned in + `/api/agent/capabilities.mcpAdapter`. Assistant, Pulse Intelligence + settings, README, and MCP surfaces may present that setup, but must not + maintain separate MCP setup constants. Security API settings may only host + the scoped-token creation link used by that setup. 10. Platform/runtime top-level pages registered in `frontend-modern/src/App.tsx` and the primary tab list in `frontend-modern/src/AppLayout.tsx` must keep the AI launcher chrome @@ -1816,7 +2738,7 @@ deriving an older display status from `workflowStatusHistory`. ## Completion Obligations -1. Update this contract when canonical AI runtime or transport entry points move, including transport-level provider request-shape changes such as OpenAI-compatible `tool_choice` handling, runtime-failure classification splits (for example separating tool-choice request rejection, no tool-capable endpoint, and generic model-level lack of tool support into distinct causes), Patrol-specific verification surfaces such as `POST /api/ai/patrol/preflight` that exercise the full chat-completions path with a minimal tool definition rather than only listing models, Patrol-preflight cache observability where the AI Service caches the most recent preflight outcome (success, soft warning, or classified failure) and the AI settings response surfaces it as `patrol_preflight` so the UI can hydrate a "last verified" indicator without forcing operators to re-run preflight on every page load, the auto-trigger contract on `HandleUpdateAISettings` where the save handler runs `TriggerPatrolPreflightAsync` only when the change actually moved Patrol transport (model swap, provider key for that model changed, or assistant just enabled with a Patrol model) so routine settings saves do not burn provider tokens, the startup-seed contract where the AI Service handler dispatches the same async preflight on Pulse boot when assistant is enabled and a Patrol model is configured so the cache is populated for the first `/api/settings/ai` poll after a restart instead of blanking back to "never verified", the readiness-integration contract where the `tools` check in the Patrol readiness payload consults the cached preflight and surfaces the classified evidence (success, soft warning, or failure with classified summary plus "last preflight ") for the configured provider+model when available (falling back to the static `PatrolToolReadinessForModel` classifier only when the cache is empty or holds a result for a different model), the stateless-Patrol-input contract where `ExecutePatrolStream` must pass only the current run's user prompt into the agentic loop rather than reloading the persisted `patrol-main` session history (so a prior run that ended with orphan `tool_calls` cannot poison every subsequent run with malformed conversation structure), and the deterministic-resolve-gate contract where the `patrol_resolve_finding` tool adapter rejects LLM-driven resolves of event/persistent category findings (`backup`, `reliability`, `security`, `general`) when a deterministic verifier exists for the finding's key and that verifier either still detects the failure signal **or returns an inconclusive result** — preventing the LLM from optimistically resolving a finding its current investigation simply didn't re-surface, which was the source of the "Backup failed" flap (detected → auto-resolved → re-detected ten times in a day before this gate). The fail-closed-on-inconclusive policy treats verifier errors (timeouts, executor unavailability, transport faults) as "we don't know" rather than "go ahead": resolution of an event/persistent finding is effectively permanent (next detection registers as a regression and inflates counters), so the safe default is to refuse and require either a successful re-verification or operator action. The gate has a symmetric counterpart, the verified stale-resolve contract: `reconcileStaleFindings` (`internal/ai/patrol_ai.go`) may auto-resolve an event/persistent finding that was seeded but neither re-reported nor resolved ONLY when the finding's key has a deterministic verifier and that verifier affirmatively confirms the failure signal is gone — absence of a re-report remains insufficient evidence for these categories, "still present" or inconclusive verification leaves the finding active (the same fail-closed default), and verifications are capped per reconcile pass (`maxVerifiedStaleResolvesPerRun`) with deferred candidates logged and retried on the next successful full patrol. Without this counterpart the lifecycle was asymmetric: a genuinely fixed backup or recovered service stayed an active finding indefinitely unless the LLM happened to call `patrol_resolve_finding`. `hasDeterministicVerifierForKey` (`internal/ai/patrol_findings.go`) is the single source of truth for which keys have verifiers, consulted by both the gate and the reconcile pass, and must stay aligned with the dispatch switch in `verifyFixDeterministically` (it previously listed two of the seven dispatch keys, silently skipping verification that existed). Finding keys normalize onto the canonical verifier vocabulary in `normalizeFindingKey` via an alias map of unambiguous directional synonyms (`high-cpu` → `cpu-high`, `high-memory` → `memory-high`, `high-disk` → `disk-high`) so deduplication and deterministic verification meet on one key, and the `patrol_report_finding` key guidance (`internal/ai/tools/tools_patrol.go`) teaches the canonical vocabulary; semantically distinct keys (`pbs-job-failed`, `node-offline`) must not be aliased onto verifier keys whose resource model they do not match, the assessment-recovery contract where the overall-health "Recent Patrol errors" coverage factor in `summarizeRecentPatrolCoverage` suppresses the score penalty once three consecutive trailing successful full Patrol runs exist at the most-recent end of the recent-runs window — so the grade reflects current reality after a Patrol-affecting bug is fixed rather than dragging stale failures forward for the ~9 hours it takes scheduled runs to age them out of the trailing-10 ratio, the orphan-tool-call-repair contract where `convertToProviderMessages` injects synthetic is_error tool result messages for any `tool_call_id` in an assistant message that has no matching downstream tool result, so a chat session that ended mid-tool-call (network drop, ctx timeout, browser crash) cannot poison its next message with the structural-violation error the provider rejects — the synthetic content is marked is_error=true and explains the interruption so the model can retry the call or proceed without the data, and the patrol-session-bound contract where `ExecutePatrolStream` calls `SessionStore.TrimMessages` after persisting each run's messages to cap the patrol-main session at 200 messages (roughly two recent runs' worth) — without the bound the file grew unbounded at every scheduled run, reaching 16 MB and 3,593 messages within a month and making every `AddMessage` rewrite linearly more expensive; the canonical Patrol forensic log is the `PatrolRunRecord` history surfaced at `/api/ai/patrol/runs`, not the chat-session-shaped file +1. Update this contract when canonical AI runtime or transport entry points move, including transport-level provider request-shape changes such as OpenAI-compatible `tool_choice` handling, runtime-failure classification splits (for example separating tool-choice request rejection, no tool-capable endpoint, and generic model-level lack of tool support into distinct causes), Patrol-specific verification surfaces such as `POST /api/ai/patrol/preflight` that exercise the full chat-completions path with a minimal tool definition rather than only listing models, Patrol-preflight cache observability where the AI Service caches the most recent preflight outcome (success, soft warning, or classified failure) and the AI settings response surfaces it as `patrol_preflight` so the UI can hydrate a "last verified" indicator without forcing operators to re-run preflight on every page load, the auto-trigger contract on `HandleUpdateAISettings` where the save handler runs `TriggerPatrolPreflightAsync` only when the change actually moved Patrol transport (model swap, provider key for that model changed, or assistant just enabled with a Patrol model) so routine settings saves do not burn provider tokens, the startup-seed contract where the AI Service handler dispatches the same async preflight on Pulse boot when assistant is enabled and a Patrol model is configured so the cache is populated for the first `/api/settings/ai` poll after a restart instead of blanking back to "never verified", the readiness-integration contract where the `tools` check in the Patrol readiness payload consults the cached preflight and surfaces the classified evidence (success, soft warning, or failure with classified summary plus "last preflight ") for the configured provider+model when available (falling back to the static `PatrolToolReadinessForModel` classifier only when the cache is empty or holds a result for a different model), the preflight-runtime-recovery contract where a successful Patrol preflight with an observed tool call resolves the synthetic Patrol runtime failure finding while failed or no-tool-call preflights leave it active, the stateless-Patrol-input contract where `ExecutePatrolStream` must pass only the current run's user prompt into the agentic loop rather than reloading the persisted `patrol-main` session history (so a prior run that ended with orphan `tool_calls` cannot poison every subsequent run with malformed conversation structure), and the deterministic-resolve-gate contract where the `patrol_resolve_finding` tool adapter rejects LLM-driven resolves of event/persistent category findings (`backup`, `reliability`, `security`, `general`) when a deterministic verifier exists for the finding's key and that verifier either still detects the failure signal **or returns an inconclusive result** — preventing the LLM from optimistically resolving a finding its current investigation simply didn't re-surface, which was the source of the "Backup failed" flap (detected → auto-resolved → re-detected ten times in a day before this gate). The fail-closed-on-inconclusive policy treats verifier errors (timeouts, executor unavailability, transport faults) as "we don't know" rather than "go ahead": resolution of an event/persistent finding is effectively permanent (next detection registers as a regression and inflates counters), so the safe default is to refuse and require either a successful re-verification or operator action. The gate has a symmetric counterpart, the verified stale-resolve contract: `reconcileStaleFindings` (`internal/ai/patrol_ai.go`) may auto-resolve an event/persistent finding that was seeded but neither re-reported nor resolved ONLY when the finding's key has a deterministic verifier and that verifier affirmatively confirms the failure signal is gone — absence of a re-report remains insufficient evidence for these categories, "still present" or inconclusive verification leaves the finding active (the same fail-closed default), and verifications are capped per reconcile pass (`maxVerifiedStaleResolvesPerRun`) with deferred candidates logged and retried on the next successful full patrol. Without this counterpart the lifecycle was asymmetric: a genuinely fixed backup or recovered service stayed an active finding indefinitely unless the LLM happened to call `patrol_resolve_finding`. `hasDeterministicVerifierForKey` (`internal/ai/patrol_findings.go`) is the single source of truth for which keys have verifiers, consulted by both the gate and the reconcile pass, and must stay aligned with the dispatch switch in `verifyFixDeterministically` (it previously listed two of the seven dispatch keys, silently skipping verification that existed). Finding keys normalize onto the canonical verifier vocabulary in `normalizeFindingKey` via an alias map of unambiguous directional synonyms (`high-cpu` → `cpu-high`, `high-memory` → `memory-high`, `high-disk` → `disk-high`) so deduplication and deterministic verification meet on one key, and the `patrol_report_finding` key guidance (`internal/ai/tools/tools_patrol.go`) teaches the canonical vocabulary; semantically distinct keys (`pbs-job-failed`, `node-offline`) must not be aliased onto verifier keys whose resource model they do not match, the assessment-recovery contract where the overall-health "Recent Patrol errors" coverage factor in `summarizeRecentPatrolCoverage` suppresses the score penalty once three consecutive trailing successful full Patrol runs exist at the most-recent end of the recent-runs window — so the grade reflects current reality after a Patrol-affecting bug is fixed rather than dragging stale failures forward for the ~9 hours it takes scheduled runs to age them out of the trailing-10 ratio, the orphan-tool-call-repair contract where `convertToProviderMessages` injects synthetic is_error tool result messages for any `tool_call_id` in an assistant message that has no matching downstream tool result, so a chat session that ended mid-tool-call (network drop, ctx timeout, browser crash) cannot poison its next message with the structural-violation error the provider rejects — the synthetic content is marked is_error=true and explains the interruption so the model can retry the call or proceed without the data, and the patrol-session-bound contract where `ExecutePatrolStream` calls `SessionStore.TrimMessages` after persisting each run's messages to cap the patrol-main session at 200 messages (roughly two recent runs' worth) — without the bound the file grew unbounded at every scheduled run, reaching 16 MB and 3,593 messages within a month and making every `AddMessage` rewrite linearly more expensive; the canonical Patrol forensic log is the `PatrolRunRecord` history surfaced at `/api/ai/patrol/runs`, not the chat-session-shaped file 2. Keep AI runtime and shared API proof routing aligned in `registry.json` 3. Preserve explicit coverage for chat, Patrol, remediation, and cost-control behavior when AI runtime changes. Interactive Assistant and Patrol tool selection must remain model-owned: Pulse may provide governed context, tools, approval state, resource-resolution facts, safety policy, and neutral resource-scoped action history, but it must not add prompt-keyword routers, expected-tool retries, auto-recovery tool calls, keyword-matched prior-fix suggestions, or Pulse-authored remediation/finding fallbacks that choose the next investigative or corrective action for the model. Assistant FSM gates remain safety boundaries after the model chooses a tool: @@ -1917,6 +2839,9 @@ deriving an older display status from `workflowStatusHistory`. metadata, while manual run requests, scheduled ticks, and scoped alert/anomaly runs must fail or skip before LLM execution when the selected Patrol model/provider is known not-ready. + Readiness checks may retain stable machine IDs such as `configuration`, but + human-facing labels in the settings payload must frame the operator boundary + as Patrol mode rather than Patrol configuration. Monitor-only Patrol autonomy saves are part of the same runtime gate: when the safe-remediation extension or entitlement is unavailable, both the browser state owner and `internal/api/ai_handlers.go` must clear stale @@ -1968,9 +2893,19 @@ deriving an older display status from `workflowStatusHistory`. 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. When the shared registry blocks a control tool in read-only mode, its - operator guidance must point to Assistant & Patrol settings and the Pulse - Assistant Permissions Control mode, not legacy Pulse Assistant settings - paths. + operator guidance must point to Pulse Intelligence > Provider & Models + settings and the Pulse Assistant Permissions Control mode, not legacy Pulse + Assistant settings paths. + Runtime status, preflight, settings-persistence, profile-suggestion, and + remediation-impact messages must preserve the same product boundary: + use Pulse Assistant only for the first-party in-app Assistant runtime itself, + Provider & Models for the shared provider/model settings surface, and Pulse + Intelligence for the shared Assistant/Patrol/agent substrate rather than + reviving the legacy generic `Pulse AI` label or `Pulse Assistant settings` + copy. Pulse Intelligence external-agent setup copy may point to Patrol mode, + but it must frame the first step as setting how autonomous Patrol should be + before connected agents request work; it must not present connected-agent + setup as a separate operations-loop, MCP-readiness, or feature-unlock model. 9. Keep self-hosted Patrol messaging aligned with the v6 GA product contract: ordinary self-hosted installs use BYOK or local providers, and the runtime must not surface retired managed-model credits, trial prompts, account-backed @@ -1978,24 +2913,24 @@ deriving an older display status from `workflowStatusHistory`. The shared app shell must also keep `/cloud` and `/cloud/signup` out of ordinary self-hosted public routes so Cloud acquisition cannot reappear as a proxy for retired hosted-model or AI quickstart activation. - The public AI overview must likewise use productized context language such + The public Pulse Intelligence overview must likewise use productized context language such as alert history, Patrol runs, and resource timelines instead of presenting `incident memory` as a standalone feature. It must also describe Patrol baselines, trends, correlations, forecasts, and deterministic signals as model-bound evidence/context rather than Pulse-authored intelligence or fallback finding creation. 10. Keep discovery-analysis prompt bounds and response budgets aligned across - `internal/ai/service.go` and the shared service-discovery prompt builders: - the runtime must reserve enough output tokens for structured discovery JSON, - and discovery prompts must cap fact/path/port fan-out explicitly instead of - relying on providers to truncate oversized infrastructure inventories. - That same runtime-owned command-target boundary must resolve hostnames - through `internal/unifiedresources/hostname_equivalence.go`. - `internal/ai/tools/internal_routing.go`, - `internal/ai/tools/tools_control.go`, and adjacent AI command helpers may - match a short host against its FQDN, but they must not broaden that - fallback into a generic short-name collapse that would make two distinct - FQDNs with the same short host look interchangeable. + `internal/ai/service.go` and the shared service-discovery prompt builders: + the runtime must reserve enough output tokens for structured discovery JSON, + and discovery prompts must cap fact/path/port fan-out explicitly instead of + relying on providers to truncate oversized infrastructure inventories. + That same runtime-owned command-target boundary must resolve hostnames + through `internal/unifiedresources/hostname_equivalence.go`. + `internal/ai/tools/internal_routing.go`, + `internal/ai/tools/tools_control.go`, and adjacent AI command helpers may + match a short host against its FQDN, but they must not broaden that + fallback into a generic short-name collapse that would make two distinct + FQDNs with the same short host look interchangeable. 11. Keep AI runtime transport compatibility separate from operator-facing product copy. Existing Patrol payload fields such as `fixed_count`, `auto_fix_model`, and `patrol_auto_fix` may remain stable wire/API names, @@ -2130,7 +3065,7 @@ deriving an older display status from `workflowStatusHistory`. container-restart, container-stop, k8s-rollout-restart, plus the Proxmox VM lifecycle classes proxmox-vm-reboot, proxmox-vm-stop, proxmox-vm-start, proxmox-vm-shutdown and the matching pct-driven - proxmox-ct-* container lifecycle classes) and return hand-authored + proxmox-ct-\* container lifecycle classes) and return hand-authored operational copy: what the command actually touches, how Pulse will read back success. The additions append onto the default safety/verification arrays rather than replacing them, so the @@ -2231,8 +3166,8 @@ deriving an older display status from `workflowStatusHistory`. whole-surface review sessions even when their bounded action references name individual findings; the session list must not infer a `patrol_finding` identity from those action references once metadata is - present. Patrol configuration failure handoffs remain - `patrol_configuration_failure` sessions and may expose only the safe + present. Patrol mode save-failure handoffs remain + `patrol_configuration_failure` sessions for compatibility and may expose only the safe runtime-failure boolean needed for browser presentation. Run-specific fields stay reserved for `patrol_run` handoffs, while hidden model context, command payloads, preflight output, and action results remain @@ -2283,11 +3218,11 @@ deriving an older display status from `workflowStatusHistory`. does not yet carry the latest approval ID. Those references are review context only: they must not include raw command text, must not grant approval or execution authority, and must route any requested action back - through the governed approval/remediation flow. Patrol provides the + through the governed approval and fix flow. Patrol provides the configured LLM with observed finding context, evidence, policy posture, and - governed action state; the LLM owns diagnosis and remediation reasoning. + governed action state; the LLM owns diagnosis and fix reasoning. Operator-visible handoffs must not describe Patrol as having already - authored the correct remediation. The finding briefing may carry only + authored the correct fix. The finding briefing may carry only factual governed action artifact metadata from those same structured references after live-approval recovery, including safe current status, request/expiry timestamps, approval policy, action plan identity, plan @@ -2403,6 +3338,62 @@ deriving an older display status from `workflowStatusHistory`. ## Current State +The canonical findings store must present one active Patrol issue per real +problem. When the model reports an equivalent active sibling under a different +finding ID, key, or severity for the same resource, `internal/ai/findings.go` +must merge it into the existing active finding before it reaches +`/api/ai/patrol/findings`, keep the highest observed severity for the merged +issue, increment recurrence, and record a bounded duplicate-merge lifecycle +event instead of creating a second operator-facing row. Distinct symptoms on +the same resource, such as CPU pressure and memory pressure, must remain +separate findings. Verification is +`go test ./internal/ai -run 'TestFindingsStore'` plus +`go test ./internal/ai -run 'TestPatrolService_GetAllFindings|TestPatrolService_GetFindingsForResource|TestPatrolService_GetAllFindingsIncludingResolved'`. + +Public Pulse Intelligence overview, safety, and onboarding copy now use the +same visible Patrol mode labels as the product: `Watch only`, +`Ask before changes`, `Auto-fix safe issues`, and `Policy autopilot`. The +legacy `Only watch`, `Fix safe issues`, and `Full control` names may remain +only as historical context or compatibility implementation details, not as customer-facing Patrol mode labels. + +Assistant provider-readiness repair actions now use the canonical Pulse +Intelligence > Provider & Models route +`/settings/pulse-intelligence/provider`. The legacy `/settings/system-ai` +route remains a compatibility alias for old deep links, not a href emitted by +new Assistant provider-repair actions. + +Rejected Patrol investigation-fix approvals are terminal governed-action +decisions in the AI runtime. `/api/ai/approvals/{id}/deny` must persist the +approval-store denial, record a rejected unified action-audit decision when the +approval carries a governed action plan, and move the owning finding to +`fix_rejected` so Assistant handoffs, Patrol summaries, and external agent +adapters see the declined fix as explicit loop state rather than a disappeared +pending approval. + +Legacy Assistant provider-tool declarations for manifest-backed Patrol finding +lifecycle tools now consume the manifest-owned raw input schemas through +`agentcapabilities.ProviderInputSchemaFromRaw`. The native Assistant runtime +may keep first-party model-facing descriptions, but it must not re-declare +required arguments such as `resolution_note` or dismissal `note` when the +canonical Pulse Intelligence manifest and API contract mark those fields +optional. +Legacy native Assistant utility provider aliases for `run_command`, `fetch_url`, +and `set_resource_url`, plus their provider JSON schemas, are owned by +`agentcapabilities.LegacyAssistantUtilityProviderTools`. The older native +Assistant service may continue to expose those compatibility aliases while the +execution migration proceeds, but it must consume the shared provider projection +and shared argument constants rather than carrying inline schema maps or local +tool-input string keys. +Native Assistant registry tools that operate on Patrol finding lifecycle state +must also consume the same `agentcapabilities` argument vocabulary for +`finding_id`, `resolution_note`, `reason`, and `note` instead of repeating +local field-name strings in their provider schema or execution maps. + +Anthropic runtime provider execution is API-key backed only. Legacy Anthropic +OAuth tokens may remain in encrypted settings solely for disconnect cleanup; +they do not make Anthropic configured, do not instantiate a provider, do not +refresh tokens, and cannot send model requests. + The done event carries `context_limit_tokens` (the active model's context window from `providers.ContextWindowTokens`) alongside token usage, and the drawer's last-turn summary renders input tokens as a percentage of that @@ -2669,13 +3660,13 @@ hidden server log. The referenced OpenCode source at fetched `dev` commit `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx`. Pulse adapts that contract at the owning stream boundary: `WorkflowStateData` carries `attempt`, `max_attempts`, and `retry_after_ms`, and `AgenticLoop` emits - `provider_retry` before sleeping between transient pre-output provider - attempts. The frontend active-turn footer and live transcript workflow row - render that same typed workflow state immediately as compact attempt/backoff - progress, counting down `retry_after_ms` from `started_at` while the turn - remains active, so the user sees Pulse moving through a retry instead of - staring at an obsolete provider-wait message. Retry workflow states are not - held behind workflow-history pacing. +`provider_retry` before sleeping between transient pre-output provider +attempts. The frontend active-turn footer and live transcript workflow row +render that same typed workflow state immediately as compact attempt/backoff +progress, counting down `retry_after_ms` from `started_at` while the turn +remains active, so the user sees Pulse moving through a retry instead of +staring at an obsolete provider-wait message. Retry workflow states are not +held behind workflow-history pacing. Assistant workflow progress is also a live typed activity row while a turn is in flight, not only hidden footer state. The referenced OpenCode source at @@ -2693,14 +3684,14 @@ same typed status, and visible assistant content, reasoning, tool, approval/question, terminal `done`, and terminal `error` events clear that row. The frontend now also keeps a bounded live-only `workflowStatusHistory` for the active assistant message so the reducer preserves state continuity while - backend preparation, context, and provider-start labels are being replaced. The - shared workflow-status presentation helper owns both the transcript row and the - active-turn composer footer so the two live surfaces show the latest canonical - workflow state immediately. Retry/backoff and stream-idle liveness states cut - through pacing because they are current route health, not neutral setup motion. - The transcript and footer therefore show current motion while the provider is - starting, retrying, or reasoning, but completed answers do not retain stale - internal-progress prose. +backend preparation, context, and provider-start labels are being replaced. The +shared workflow-status presentation helper owns both the transcript row and the +active-turn composer footer so the two live surfaces show the latest canonical +workflow state immediately. Retry/backoff and stream-idle liveness states cut +through pacing because they are current route health, not neutral setup motion. +The transcript and footer therefore show current motion while the provider is +starting, retrying, or reasoning, but completed answers do not retain stale +internal-progress prose. Stream-idle heartbeats are part of that same visible workflow state. When the frontend knows the selected provider/model route from the backend workflow event or the streaming assistant message, the idle heartbeat must render as @@ -2754,8 +3745,8 @@ tool metadata updates in `packages/opencode/src/session/tools.ts` lines 54-63, while adapting the interaction to Pulse's single active-turn plus queued follow-up safety model. -Assistant command access follows the same OpenCode-referenced footer principle: -commands are reachable from prompt-adjacent chrome, not only from a hidden slash +Assistant chat actions follow the same OpenCode-referenced footer principle: +actions are reachable from prompt-adjacent chrome, not only from a hidden slash draft. The referenced OpenCode source at fetched `dev` commit `e82542b8023a8374f29c23b70ec019c8f256354e` builds the direct command surface in `packages/opencode/src/cli/cmd/run/footer.command.tsx` (`RunCommandMenuBody` @@ -3042,6 +4033,11 @@ usage and spend backing Pulse Assistant and Patrol rather than generic `AI` history, and `frontend-modern/src/utils/aiCostPresentation.ts` must own the title, empty/loading states, budget note, and reset/export history messaging so settings shells and runtime widgets do not fork their own usage wording. +Usage grouping rows must present product concepts such as Assistant sessions, +Patrol runs, Discovery runs, or concrete resource labels. Raw target storage +keys, opaque session identifiers, UUIDs, and values such as +`assistant_session_title:` are accounting implementation detail and must +stay out of the operator-facing table. That same runtime-facing table ownership applies to the cost dashboard shell: `frontend-modern/src/components/AI/AICostDashboard.tsx` owns provider usage, budget, and history semantics, but its tabular presentation must compose the @@ -3249,6 +4245,9 @@ for provider naming, provider health labels, control-level semantics, chat drawer title/subtitle, launcher title/aria copy, session-menu labeling, discovery hint framing, chat/session empty states, assistant message and question-card labels. +Discovery hint framing must follow the Pulse Intelligence settings IA: use the +simple `Discovery` label, and explain the concrete service context it unlocks, +without promoting workload discovery as another first-class product surface. Settings and chat surfaces must consume those helpers instead of keeping local AI wording or model/provider inference branches. Assistant chat must not render Pulse-authored explore pre-pass cards or @@ -3378,7 +4377,12 @@ handoffs preserve Patrol provenance before later action-audit hydration refreshe the current action state. Backend chat refresh of a Patrol finding handoff must hydrate the same requester identity directly from the live approval record, so Assistant does not depend on browser-authored metadata to distinguish -Patrol-origin proposals from generic Assistant actions. +Patrol-origin proposals from generic Assistant actions. Rejected Patrol +investigation-fix approvals must also enter that shared decision lifecycle: +`/api/ai/approvals/{id}/deny` records a rejected unified action-audit decision +when the approval has a governed action plan and moves the owning Patrol +finding to `fix_rejected`, so a declined fix remains a visible governed loop +outcome rather than disappearing when the pending approval leaves the queue. The same ownership includes the Pulse query tool schema under `internal/ai/tools/`: topology-query input names must stay canonical inside the AI runtime itself, so new tool arguments such as `max_proxmox_nodes` @@ -3965,7 +4969,7 @@ legacy managed-credit block conditions remain part of the governed runtime contract instead of being inferred later from the last successful patrol summary. When missing provider configuration blocks Patrol, `blocked_reason` must point -to Assistant & Patrol provider settings and tool-capable Patrol model +to Pulse Intelligence > Provider & Models settings and tool-capable Patrol model selection. That runtime-state contract must be derived from live Patrol runtime inputs, not only from the last failed run attempt, and the backend must clear any stale @@ -3976,6 +4980,12 @@ derive `Health A` or `100/100` from "no active findings" alone when recent Patrol evidence is limited to alert-scoped runs or includes recent Patrol run errors; the summary must degrade and explain that overall infrastructure health is not fully verified until a recent successful full Patrol run exists. +The backend may keep precise full-run, scoped-run, and verification terms in +wire fields and internal decisions, but browser-facing summary and handoff copy +emitted from `internal/ai` should translate that precision into check language: +recent broad checks, targeted checks, follow-up checks, current issues, and +verified outcomes. Runtime explanations must not surface activation-loop, +proof-strip, or scoped/full-run jargon as the ordinary operator vocabulary. That coverage explanation must also stay faithful to the actual recent run shape. When the most recent verification evidence includes a full Patrol run that ended with errors, the health summary must say that a recent full patrol @@ -4008,17 +5018,21 @@ so the full-run seed/reconcile path must not auto-resolve them as `Resource no longer exists in infrastructure` just because `ai-service` is not present in the infrastructure snapshot. Those findings stay active until Patrol actually succeeds or resolves them for a Patrol-owned reason. -That success boundary includes provider-backed scoped Patrol runs. A successful -scoped run proves that Patrol can currently reach the selected provider/model -and complete tool-backed analysis, so it must clear the synthetic -`ai-service` runtime failure just as a successful full Patrol run does, without -loosening ordinary scoped finding reconciliation for infrastructure issues. +That success boundary includes provider-backed scoped Patrol runs and successful +Patrol tool-call preflights. A successful scoped run proves that Patrol can +currently reach the selected provider/model and complete tool-backed analysis; +a successful preflight with an observed tool call proves the configured +provider/model currently accepts Patrol's tool-call path. Either must clear the +synthetic `ai-service` runtime failure just as a successful full Patrol run +does, without loosening ordinary scoped finding reconciliation for +infrastructure issues. A soft-warning preflight where the provider responds but +the model does not emit a tool call is not sufficient recovery evidence. Because those findings represent Patrol blindness rather than operator-triaged infrastructure noise, the Patrol runtime must also reject manual acknowledge, snooze, dismiss, resolve, and suppress actions against synthetic `ai-service` runtime findings. The canonical recovery path is to correct Patrol provider -configuration in Assistant & Patrol settings and let Patrol re-evaluate the -runtime condition on the next run. +configuration in Pulse Intelligence > Provider & Models settings and let Patrol +re-evaluate the runtime condition on the next run. The shared findings lifecycle must also treat a regressed issue as a new active occurrence. When a resolved finding reappears, `internal/ai/findings.go` must clear any stale acknowledgement timestamp from the prior occurrence instead of @@ -4227,6 +5241,17 @@ after every `loop.ExecuteWithTools` return — success or error, since the operator was billed regardless of whether the loop produced a clean response. It emits a `cost.UsageEvent` with `UseCase="chat"` in the same shape the rest of the runtime uses. +When a chat turn carries a product-originated finding, resource, handoff, +action, or structured-mention context, the event may also include the coarse +`context_scope` classifier so privacy-safe telemetry can count governed-context +Assistant collaboration without exporting prompts, session IDs, resource IDs, +finding IDs, command text, or action payloads. +When the same turn includes accepted model-selected governed tool calls, the +agentic loop records only the total count on the resulting cost event. That +count supports Pulse Intelligence adoption telemetry for Assistant +collaboration while keeping tool names, arguments, results, provider call IDs, +transcript content, resource IDs, finding IDs, command text, action payloads, +and session IDs out of persisted/exported telemetry. The store is threaded through `chat.Config.CostStore`, wired by the router from the per-tenant `AISettingsHandler.GetAIService` via `Service.CostStore()`. `ExecutePatrolStream` deliberately diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index a0e12ffac..d87ea8359 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -25,100 +25,113 @@ the trust boundary that keeps customer-safe support diagnostics and normal product API routes free of maintainer commercial analytics. ## Canonical Files + 1. `internal/api/contract_test.go` -1a. `internal/api/platform_connection_shared.go` -1b. `internal/api/metadata_handlers_shared.go` + 1a. `internal/api/platform_connection_shared.go` + 1b. `internal/api/metadata_handlers_shared.go` 2. `internal/api/resources.go` 3. `internal/api/discovery_handlers.go` -3. `internal/api/alerts.go` -4. `internal/api/activity_audit_handlers.go` -5. `internal/api/actions.go` -5a. `internal/api/action_executor.go` -5b. `internal/api/docker_container_action_executor.go` -5c. `internal/api/proxmox_guest_action_executor.go` -6. `internal/actionplanner/planner.go` -7. `pkg/pulsecli/api_client.go` -8. `pkg/pulsecli/actions.go` -9. `pkg/pulsecli/fleet.go` -10. `pkg/pulsecli/root.go` -5. `frontend-modern/src/types/api.ts` -6. `frontend-modern/src/types/actionAudit.ts` -7. `frontend-modern/src/api/actionAudit.ts` -7a. `frontend-modern/src/api/resourceActions.ts` -6. `frontend-modern/src/api/responseUtils.ts` -7. `frontend-modern/src/components/Settings/APITokenManager.tsx` -8. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` -9. `frontend-modern/src/constants/apiScopes.ts` -9. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx` -10. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` -11. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` -12. `frontend-modern/src/components/Settings/NodeModalAuthenticationSection.tsx` -15. `frontend-modern/src/components/Settings/NodeModalBasicInfoSection.tsx` -16. `frontend-modern/src/components/Settings/nodeModalModel.ts` -17. `frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx` -18. `frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx` -19. `frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx` -20. `frontend-modern/src/components/Settings/useNodeModalState.ts` -21. `frontend-modern/src/utils/agentInstallCommand.ts` -22. `frontend-modern/src/api/nodes.ts` -23. `frontend-modern/src/api/license.ts` -24. `frontend-modern/src/api/monitoredSystemLedger.ts` -25. `frontend-modern/src/api/resources.ts` -26. `frontend-modern/src/api/monitoring.ts` -27. `internal/api/monitored_system_ledger.go` -28. `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx` -29. `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts` -31. `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts` -32. `frontend-modern/src/utils/apiTokenPresentation.ts` -33. `frontend-modern/src/utils/infrastructureSettingsPresentation.ts` -34. `internal/api/router_routes_auth_security.go` -35. `internal/api/relay_hosted_runtime.go` -36. `internal/api/ai_hosted_runtime.go` -37. `internal/api/router_routes_licensing.go` -38. `internal/api/reporting_inventory_handlers.go` -39. `internal/cloudcp/portal/bootstrap.go` -40. `internal/cloudcp/portal/handlers.go` -41. `internal/cloudcp/portal/page.go` -42. `internal/cloudcp/portal/page_templates.go` -43. `internal/cloudcp/portal/frontend/src/index.ts` -44. `internal/cloudcp/portal/frontend/src/shell.ts` -45. `internal/cloudcp/portal/frontend/src/billing.ts` -46. `internal/cloudcp/portal/frontend/src/runtime.ts` -47. `internal/cloudcp/portal/frontend/src/types.ts` -48. `internal/cloudcp/portal/frontend/src/styles.css` -49. `internal/cloudcp/portal/frontend/tsconfig.json` -50. `internal/cloudcp/portal/frontend_sync_test.go` -51. `internal/api/recovery_handlers.go` -51a. `internal/api/pbs_backups.go` -52. `internal/api/config_setup_handlers.go` -52a. `internal/api/setup_script_render.go` -52b. `internal/api/cloud_agent_install_command.go` -53. `internal/api/demo_mode_commercial.go` -54. `internal/api/demo_mode_operations.go` -55. `internal/api/security_status_capabilities.go` -56. `internal/api/demo_middleware.go` -56. `frontend-modern/src/stores/aiRuntimeState.ts` -57. `internal/api/connections_types.go` -58. `internal/api/connections_aggregator.go` -59. `internal/api/connections_handlers.go` -60. `internal/api/connections_probe.go` -61. `frontend-modern/src/api/connections.ts` -62. `frontend-modern/src/utils/connectionErrorPresentation.ts` -63. `internal/api/availability_handlers.go` -64. `frontend-modern/src/api/availabilityTargets.ts` -65. `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx` -65a. `frontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsx` -65b. `frontend-modern/src/components/Settings/availabilitySettingsModel.ts` -66. `pkg/aicontracts/investigation.go` -67. `internal/api/ai_intelligence_handlers.go` -68. `frontend-modern/src/api/ai.ts` -69. `frontend-modern/src/api/aiChat.ts` -69a. `frontend-modern/src/api/aiChatDevStreamFixture.ts` -70. `frontend-modern/src/api/patrol.ts` -71. `frontend-modern/src/api/generated/aiChatEvents.ts` -72. `internal/api/agent_exec_token_binding.go` -73. `internal/agentcontext/` -74. `scripts/generate-types.go` +4. `internal/api/alerts.go` +5. `internal/api/activity_audit_handlers.go` +6. `internal/api/actions.go` + 5a. `internal/api/action_executor.go` + 5b. `internal/api/docker_container_action_executor.go` + 5c. `internal/api/proxmox_guest_action_executor.go` +7. `internal/actionplanner/planner.go` +8. `pkg/pulsecli/api_client.go` +9. `pkg/pulsecli/actions.go` +10. `pkg/pulsecli/fleet.go` +11. `pkg/pulsecli/root.go` +12. `frontend-modern/src/types/api.ts` +13. `frontend-modern/src/types/actionAudit.ts` +14. `frontend-modern/src/api/actionAudit.ts` + 7a. `frontend-modern/src/api/resourceActions.ts` + 7b. `frontend-modern/src/api/agentCapabilities.ts` + 7c. `frontend-modern/src/api/generated/agentCapabilities.ts` +15. `frontend-modern/src/api/responseUtils.ts` +16. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` +17. `frontend-modern/src/components/Settings/APITokenManager.tsx` +18. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` +19. `frontend-modern/src/constants/apiScopes.ts` +20. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx` +21. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` +22. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` +23. `frontend-modern/src/components/Settings/NodeModalAuthenticationSection.tsx` +24. `frontend-modern/src/components/Settings/NodeModalBasicInfoSection.tsx` +25. `frontend-modern/src/components/Settings/nodeModalModel.ts` +26. `frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx` +27. `frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx` +28. `frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx` +29. `frontend-modern/src/components/Settings/useNodeModalState.ts` +30. `frontend-modern/src/utils/agentInstallCommand.ts` +31. `frontend-modern/src/api/nodes.ts` +32. `frontend-modern/src/api/license.ts` + 23a. `frontend-modern/src/api/settings.ts` +33. `frontend-modern/src/api/monitoredSystemLedger.ts` +34. `frontend-modern/src/api/resources.ts` +35. `frontend-modern/src/api/monitoring.ts` +36. `internal/api/monitored_system_ledger.go` +37. `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx` +38. `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts` +39. `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts` +40. `frontend-modern/src/utils/apiTokenPresentation.ts` +41. `frontend-modern/src/utils/infrastructureSettingsPresentation.ts` +42. `internal/api/router_routes_auth_security.go` +43. `internal/api/relay_hosted_runtime.go` +44. `internal/api/ai_hosted_runtime.go` +45. `internal/api/router_routes_licensing.go` +46. `internal/api/reporting_inventory_handlers.go` +47. `internal/cloudcp/portal/bootstrap.go` +48. `internal/cloudcp/portal/handlers.go` +49. `internal/cloudcp/portal/page.go` +50. `internal/cloudcp/portal/page_templates.go` +51. `internal/cloudcp/portal/frontend/src/index.ts` +52. `internal/cloudcp/portal/frontend/src/shell.ts` +53. `internal/cloudcp/portal/frontend/src/billing.ts` +54. `internal/cloudcp/portal/frontend/src/runtime.ts` +55. `internal/cloudcp/portal/frontend/src/types.ts` +56. `internal/cloudcp/portal/frontend/src/styles.css` +57. `internal/cloudcp/portal/frontend/tsconfig.json` +58. `internal/cloudcp/portal/frontend_sync_test.go` +59. `internal/api/recovery_handlers.go` + 51a. `internal/api/pbs_backups.go` +60. `internal/api/config_setup_handlers.go` + 52a. `internal/api/setup_script_render.go` + 52b. `internal/api/cloud_agent_install_command.go` +61. `internal/api/demo_mode_commercial.go` +62. `internal/api/demo_mode_operations.go` +63. `internal/api/security_status_capabilities.go` +64. `internal/api/demo_middleware.go` +65. `frontend-modern/src/stores/aiRuntimeState.ts` +66. `internal/api/connections_types.go` +67. `internal/api/connections_aggregator.go` +68. `internal/api/connections_handlers.go` +69. `internal/api/connections_probe.go` +70. `frontend-modern/src/api/connections.ts` +71. `frontend-modern/src/utils/connectionErrorPresentation.ts` +72. `internal/api/availability_handlers.go` +73. `frontend-modern/src/api/availabilityTargets.ts` +74. `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx` + 65a. `frontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsx` + 65b. `frontend-modern/src/components/Settings/availabilitySettingsModel.ts` +75. `pkg/aicontracts/investigation.go` + 66a. `pkg/aicontracts/orchestrator_deps.go` + 66b. `pkg/aicontracts/fix_execution.go` +76. `internal/api/ai_intelligence_handlers.go` +77. `frontend-modern/src/api/ai.ts` +78. `frontend-modern/src/api/aiChat.ts` + 69a. `frontend-modern/src/api/aiChatDevStreamFixture.ts` +79. `frontend-modern/src/api/patrol.ts` +80. `frontend-modern/src/api/generated/aiChatEvents.ts` +81. `internal/api/agent_exec_token_binding.go` + 72a. `cmd/pulse-mcp/main.go` + 72b. `cmd/pulse-mcp/README.md` + 72c. `cmd/agent-probe/main.go` + 72d. `scripts/generate-pulse-intelligence-docs.go` +82. `internal/agentcontext/` + 73a. `internal/agentcapabilities/` + 73b. `pkg/extensions/ai_autofix.go` +83. `scripts/generate-types.go` ## Shared Boundaries @@ -126,6 +139,285 @@ Assistant chat `workflow_state` events are a shared AI/API payload boundary. The backend source of truth is `internal/ai/chat.WorkflowStateData`, generated to `frontend-modern/src/api/generated/aiChatEvents.ts` by `scripts/generate-types.go` and pinned by `internal/api/contract_test.go`. +Agent capabilities manifest payloads follow the same rule: +`internal/agentcapabilities.Manifest`, `Capability`, `CapabilityCategory`, +`SurfaceContract`, `SurfaceContractComponent`, `OperatorSurfaceContract`, +`SurfaceAffordanceContract`, `MCPAdapterContract`, and +`MCPAdapterConfigFamily` generate +`frontend-modern/src/api/generated/agentCapabilities.ts`; the shared frontend +client must alias those generated types instead of redeclaring a browser-local +manifest model. The manifest-owned `surfaceContract` is the +canonical API statement that Pulse Intelligence Core is shared, Pulse Patrol is +the primary built-in operator, and Pulse Assistant plus Pulse MCP are the +supported access paths over the same governed capabilities, including the +affordances each path exposes. The Patrol surface description is also +manifest-owned product copy; API clients, docs, and settings projections must +render the concise policy-bound operator summary from the manifest instead of +carrying local control-level prose. +The manifest-owned `mcpAdapter` is the canonical API statement for Pulse MCP +client setup facts: server name, command, base URL flag/default, token +environment variable, and supported client config families. UI and docs may +compose copy around those facts, but they must not carry component-local or +README-local setup constants. +Manifest-backed MCP initialize responses must render that relationship from +`Manifest.SurfaceContract` rather than hard-coded adapter prose. Native +Assistant operating instructions must use the same shared renderer, with +surface-specific affordance flags controlling only whether tools, resources, +prompts, capability metadata, and Assistant-native interactive questions are +advertised. MCP initialize responses and JSON-RPC handlers must resolve those +surface affordances before advertising or serving tools, resources, prompts, or +capability metadata; raw manifest capability and prompt presence can satisfy an +allowed affordance, but cannot enable a disabled surface affordance. Prompt +advertising and serving must use `MCPManifestSurfacePromptProjectionSupported` +and `GetMCPPromptFromManifestSurface` so the target surface's `prompts` +affordance and the manifest-owned workflow prompt catalogue are resolved +together before initialize, `prompts/list`, or `prompts/get` exposes prompts. +The Pulse Intelligence manifest docs generator must also render the MCP README +surface-contract block from `Manifest.SurfaceContract` and the MCP client +config block from `Manifest.MCPAdapter`; scope, tool, prompt, error-code, and +Assistant/MCP/Core/Patrol relationship docs are all API-manifest projections, +not parallel onboarding snapshots. +Native Assistant workflow-prompt rendering is a canonical API projection of +that same manifest. `POST /api/ai/workflow-prompts/render` is an authenticated +`ai:chat` route owned by `internal/api/ai_handler.go` and +`internal/api/router_routes_ai_relay.go`; it accepts the manifest prompt name +plus argument map, validates them against the manifest-owned workflow prompt +catalogue through `internal/agentcapabilities`, and returns only the rendered +Assistant prompt text plus description. Browser +clients must read prompt names, display labels, presentation kind hints, +descriptions, and arguments through the generated `workflowPrompts` manifest +type and call `AIChatAPI.renderWorkflowPrompt` rather than rendering prompt +templates locally. Browser context may carry a preferred workflow prompt name +only as a request to render one manifest-owned starter through that API route; +it is not a transport for prompt text or a reason to bypass argument resolution. +MCP `prompts/list` projects those same labels as protocol prompt titles and MCP +`prompts/get` remains the protocol-edge projection over the same shared +workflow-prompt core. Route inventory and AI chat scope tests must pin that +route so it does not drift into public manifest discovery, relay-mobile access, +or `ai:execute` approval replay. +Successful workflow prompt rendering is also the API boundary for content-free +guided-starter activity: native Assistant rendering records only the manifest +prompt name, coarse Assistant surface, and timestamp; first-party Pulse app +surfaces that start the same governed journey without rendering prompt text may +record through authenticated `POST /api/ai/workflow-prompts/activity`; and MCP +adapters that render prompts locally may report the same event through +`POST /api/agent/workflow-prompt-activity`. The first-party marker route is an +authenticated `ai:chat` route owned by `internal/api/ai_handler.go` and +`internal/api/router_routes_ai_relay.go`; it accepts one JSON object with a +manifest-declared prompt name plus an optional first-party surface, allows only +`pulse_assistant`, `pulse_patrol`, `patrol_control`, `patrol_autonomy`, and +the legacy `pulse_pro_activation` alias, defaults omitted surface to `pulse_assistant`, +rejects unknown prompts or non-first-party surfaces, and returns no payload. +`pulse_patrol` is the ordinary first-party Patrol entry marker, while +`patrol_control` is the current paid Patrol control handoff marker for the +same manifest-owned Patrol work prompt. `patrol_autonomy` and +`pulse_pro_activation` remain legacy Pro activation entry-point aliases for +compatibility only; none of these surfaces is a separate prompt, route, +Assistant surface, or completion proof. +The browser-only paid Patrol control handoff may carry the coarse +`patrolControlStarter=patrol_control` route query so Patrol can record that +same marker after navigation. The old `operationsLoopStarter=patrol_control`, +`operationsLoopStarter=patrol_autonomy`, and +`operationsLoopStarter=pulse_pro_activation` values must remain accepted as +legacy aliases that normalize to the current Patrol control starter, but those +queries are not API payload fields and may not contain prompt arguments, +resource IDs, finding IDs, account identity, token identity, or terminal proof. +Consuming that route handoff must still enter the authenticated first-party +marker route described here before any status projection treats it as Patrol +control starter evidence. Operations status projections count `pulse_patrol`, +`patrol_control`, legacy `patrol_autonomy`, and legacy `pulse_pro_activation` +as Patrol control starter evidence, while `proActivationOperationsLoopStarterCount` +preserves only the legacy Pro activation entry-point count. The +agent marker route is an authenticated `monitoring:read` API-token route, +accepts only one JSON object with a manifest-declared prompt name, normalizes +the `X-Pulse-Agent-Surface` value `pulse_mcp` to the Pulse MCP surface, and +must reject unknown prompts without recording activity. Neither route may +accept or persist prompt arguments, rendered text, resource IDs, finding IDs, +session IDs, token identity, request bodies, or model output. +Telemetry preview and outbound ping payloads project that same content-free +activity into 30-day Patrol work starter counters: one total starter count +plus source-specific Assistant, Pulse Patrol, primary Patrol control, legacy +Pro activation entry-point, and Pulse MCP counts. Those payload fields are +API-contract fields for adoption/value measurement only; they must not carry +prompt text, prompt arguments, resource/finding identity, session identity, +checkout/account identity, or request details. +`GET /api/agent/patrol-control/status` owns the shared content-safe Patrol +control value state exposed to native Pulse and MCP-facing agents. The legacy +`GET /api/agent/operations-loop/status` URL remains a compatibility alias, not +the primary route: +`not_started`, `in_progress`, `governed_decision_recorded`, +`verified_needs_mcp`, and `verified`. `verified_needs_mcp` remains a tolerated +legacy value for older payloads, but MCP readiness is no longer a Patrol +control value gate. Counts remain the aggregate evidence source, while +`patrolControlValueState` is the canonical branch signal for distinguishing a +safe terminal rejection from a verified operations-value outcome. +`patrolAutonomyValueState` and +`proActivationValueProofState` mirror that value as compatibility aliases for +older commercial and telemetry consumers. Only `verified` means the first-party +Patrol control loop has approved-governed action, verified outcome proof, and +recorded action history; adjacent UI or agent clients must not infer that from +`nextAction=complete` alone. +Current-work precedence is part of that same contract: `activeFindingCount` must +include aggregate active issue-level findings from the canonical Patrol findings +store, including runtime/service findings that are not attached to a unified +registry resource. Pending approvals and active findings are current operator +work and must outrank older completed/resolved loop proof when the route chooses +`nextAction` and step status. Historical Patrol control proof remains recorded +as count-only value evidence, but it must render as history until the current +finding or approval is handled. +The authenticated `GET /api/agent/patrol-control/status` projection exposes the same content-free starter evidence as count-only fields +(`operationsLoopStarterCount`, `assistantOperationsLoopStarterCount`, +`patrolOperationsLoopStarterCount`, +`patrolControlOperationsLoopStarterCount`, +`patrolControlCompletedOperationsLoopCount`, +`patrolControlResolvedOperationsLoopCount`, `patrolControlValueState`, +`patrolAutonomyOperationsLoopStarterCount`, +`patrolAutonomyCompletedOperationsLoopCount`, +`patrolAutonomyResolvedOperationsLoopCount`, `patrolAutonomyValueState`, +`proActivationOperationsLoopStarterCount`, +`proActivationCompletedOperationsLoopCount`, +`proActivationResolvedOperationsLoopCount`, and `mcpOperationsLoopStarterCount`) +inside the status evidence window. These fields are orientation/adoption +evidence for Assistant, Patrol control, older compatibility clients, and MCP +loop entry points. `patrolControlOperationsLoopStarterCount` is the primary +starter count for native Pulse Patrol and Patrol control starts, while +`patrolAutonomyOperationsLoopStarterCount` mirrors it for compatibility and +`proActivationOperationsLoopStarterCount` is retained only for older +external-agent clients; primary clients must use +`patrolControlOperationsLoopStarterCount`. Only the completed-loop and +resolved-loop counts may +summarize that first-party starter evidence and terminal loop evidence +coexisted in the current evidence window. `patrolControlCompletedOperationsLoopCount` +requires Patrol issue evidence, contextual Assistant or external-agent +collaboration, and either a rejected governed decision or an approved governed +decision with verified outcome proof. `patrolControlResolvedOperationsLoopCount` +is stricter: it requires Patrol issue evidence, contextual Assistant or +external-agent collaboration, an approved governed decision, and verified +outcome proof. The completed-loop, resolved-loop, and value-state +`patrolAutonomy*` and `proActivation*` fields are compatibility aliases for the +same Patrol control proof model; human-facing manifest descriptions and +external-agent docs must describe all of these fields as Patrol control +compatibility metadata rather than a Pro activation journey. The +`internal/telemetry.ClassifyPulseIntelligencePatrolControlProof` helper is the +canonical count-only classifier for those completed/resolved counts and the +`patrolControlValueState` string; API status projections and outbound +telemetry/adoption projections must call that helper or its legacy alias +wrapper instead of restating the branch rules locally, and must not pass +`externalAgentReady` into that classifier because MCP readiness is not a Patrol +control proof input. The +`externalAgentReady` field is not manifest-shape readiness by itself: the route +must first prove the Pulse MCP Patrol work contract exists in the canonical +manifest and then require a single non-expired API token covering every scope +required by the published Pulse MCP Patrol work capability set. It is +optional external-agent readiness for settings handoff and MCP clients, not a +completed/resolved Patrol control proof requirement. They must +not expose +prompt names, surfaces, identities, payloads, checkout/account +identity, route parameters, action IDs, command text, resource names, or +finding IDs, and they do not replace the detail-owning Assistant, Patrol, +governed-action, verification, or finding-resolution routes. +The public `docs/AI.md` Pulse Intelligence overview must use the same API +projection: its marked Core/Patrol/Assistant/MCP block is rendered by +`PulseIntelligenceOverviewMarkdown(Manifest.SurfaceContract)`, while +surrounding product detail may stay human-authored. +The External agents settings panel is part of the same API projection: its +surface summary must consume the generated frontend manifest types and +`/api/agent/capabilities.surfaceContract` through +`frontend-modern/src/api/agentCapabilities.ts` instead of repeating +Assistant/MCP/Core/Patrol relationship copy or affordance labels in the +component. +Its MCP client examples and copied config snippets must also consume +`/api/agent/capabilities.mcpAdapter` through that same shared frontend client; +the component must not own local `pulse-mcp`, `--base-url`, token-env, or +client-family constants, and copied config snippets should be available only +inside an on-demand setup disclosure rather than becoming the default API Access +page content. +The public investigation orchestrator dependency contract in +`pkg/aicontracts/orchestrator_deps.go` is also part of this shared boundary: +orchestrator tool-call and tool-result payloads must bridge to the shared +`agentcapabilities.ProviderToolCall` / `ProviderToolResult` shapes through the +`aicontracts` projection helpers, preserving Assistant/MCP input-map, +thought-signature, and provider-result normalization instead of duplicating +local payload copies in API handlers. +The API contract proof for native Assistant provider tools follows the same +shared-boundary rule: chat-facing API tests must pin +`PulseToolExecutor.AssistantProviderTools` as the single native Assistant +provider-tool projection entrypoint, while the executor owns the manifest +surface-affordance gate and the pairing of runtime-available registry tools, +registry governance descriptors, and Assistant-native interaction tools before +provider requests are assembled. +Surface tool inventories are a shared Pulse Intelligence API contract rather +than a UI or adapter inference. `internal/agentcapabilities.SurfaceToolContract` +and `ProjectPulseIntelligenceSurfaceToolContracts` must describe Pulse +Assistant tools as `assistant_registry` sourced provider tools, splitting +registry-backed tools from Assistant-native interaction tools such as +`pulse_question`, while manifest-declared external-adapter tools are +`capability_manifest` sourced request/response capabilities projected from +`Manifest.Capabilities` by `ProjectManifestSurfaceToolContracts` and published +on `Manifest.SurfaceToolContracts`. External surfaces, including Pulse MCP, +must resolve through the surface-generic manifest contract path and must fail +closed when their published `surfaceToolContracts` entry is missing. Native +Assistant provider-tool projection must enter through +`ProjectPulseAssistantProviderTools`, so disabled Assistant affordances also +fail closed instead of letting runtime registry availability re-enable provider +tools. The two surfaces therefore share governance vocabulary and core +execution contracts without pretending their tool-name lists are +interchangeable. +The frontend-facing contract type must be generated from that same Go shape +(`frontend-modern/src/api/generated/agentCapabilities.ts`) and re-exported by +the shared agent-capabilities client. Do not hand-write a parallel frontend +copy. Runtime surface-tool normalization and count/presentation helpers also +belong in `frontend-modern/src/api/agentCapabilities.ts`, so native Assistant +UI, MCP onboarding surfaces, and future agent-compatible surfaces do not infer +tool availability from local tool-name lists. Do not expose live Assistant +registry availability through the public, cacheable `/api/agent/capabilities` +manifest; that runtime inventory belongs behind the authenticated +`GET /api/ai/assistant/surface-tools` Assistant surface endpoint when the UI +needs it. The Pulse Assistant shell may render a runtime tool-posture chip, but +it must call `AIChatAPI.getAssistantSurfaceTools()` and +`getAgentSurfaceToolPosturePresentation` instead of hard-coding registry tool +names or treating MCP capability names as native Assistant tools. The Agent +integrations panel may render MCP tool posture from the static public manifest, +but the request/response capability filtering and `SurfaceToolContract` +construction must be backend-owned in `Manifest.SurfaceToolContracts`; the +generic `getAgentManifestSurfaceToolContract(manifest, +AGENT_SURFACE_ID_PULSE_MCP)` frontend client path may normalize only that +published manifest field. Missing `surfaceToolContracts` entries must not cause +the browser to infer MCP tools from raw capabilities. The panel must not know the +streaming-only `subscribe_events` exception, call a per-surface projection +alias, or maintain a local MCP tool count. The shared frontend client must not +export a Pulse-MCP-specific surface tool helper; MCP onboarding uses the generic +surface resolver with `AGENT_SURFACE_ID_PULSE_MCP` so future external surfaces do +not require browser-side helper duplication. +Frontend surfaces that need to know whether Pulse MCP exposes the full Patrol +work loop must derive that readiness through the shared agent-capabilities +client from the manifest-owned operations-loop workflow prompt, MCP adapter +contract, and Pulse MCP surface tool contract rather than hard-coding readiness +in the consuming component. The Patrol work capability set includes the +content-safe `get_patrol_control_status` capability as the first orientation +read, followed by fleet context, resource context, findings, governed action +planning, action decision, action execution, and finding resolution; readiness +helpers, workflow-prompt gates, and MCP surface projections must consume that +one shared set instead of maintaining local loop-tool lists. Native Patrol +surfaces that need the current loop evidence must call the authenticated +`GET /api/agent/patrol-control/status` route through +`frontend-modern/src/api/agentCapabilities.ts`, not through a +Patrol-local fetcher or a browser-side reconstruction of agent capability +progress. First-party Patrol presentation may tolerate legacy `open_mcp` and +`verified_needs_mcp` payloads as already verified operator-loop history, and it +may show `externalAgentReady` only as optional external-agent context. Patrol +current-work state/model must not fetch the manifest to derive MCP readiness, +accept MCP readiness props, or introduce a page-local MCP readiness contract. +First-party Patrol UI may present that projection as Patrol current work, but +the authenticated route, query compatibility aliases, and generated wire fields +remain the `operations-loop` contract until a deliberate API migration changes +them. +Backend consumers that need an external surface's request/response capability +set must resolve it through `ResolveManifestSurfaceToolContract` and +`ManifestSurfaceToolCapabilities`, not by reading raw `surfaceToolContracts` +entries directly; that shared path normalizes `CapabilityNames`/`ToolNames`, +filters non-request/response capabilities, and enforces the resolved surface +`tools` affordance before docs, adapters, or UI projections see any tools. Provider retry progress must use typed fields (`attempt`, `max_attempts`, `retry_after_ms`) on that same event instead of frontend-only string parsing or provider-specific ad hoc events; the Assistant UI may format those fields, but @@ -334,38 +626,280 @@ templates are collapsible guidance over the account payload; they must not replace tenant-local setup facts, become a readiness source, or require a payload shape change when the portal presents compact client rows. -1. `frontend-modern/src/api/agentProfiles.ts` shared with `agent-lifecycle`: the agent profiles frontend client is both an agent lifecycle control surface and a canonical API payload contract boundary. -2. `frontend-modern/src/api/ai.ts` shared with `ai-runtime`: the AI frontend client is both an AI runtime control surface and a canonical API payload contract boundary. -3. `frontend-modern/src/api/nodes.ts` shared with `agent-lifecycle`: the shared Proxmox node client is both an agent lifecycle setup/install control surface and a canonical API payload contract boundary. -4. `frontend-modern/src/api/notifications.ts` shared with `notifications`: the notifications frontend client is both a notification delivery control surface and a canonical API payload contract boundary. -5. `frontend-modern/src/api/orgs.ts` shared with `organization-settings`: the organization frontend client is both an organization settings control surface and a canonical API payload contract boundary. -6. `frontend-modern/src/api/patrol.ts` shared with `ai-runtime`: the Patrol frontend client is both an AI runtime control surface and a canonical API payload contract boundary. - The Patrol status payload owns Patrol readiness as structured API state: - provider/model/settings/tool prerequisites must travel as bounded readiness - checks instead of frontend-only heuristics or generic analysis-failed text. - `/api/settings/ai/update` must persist recoverable provider/model changes - and return the same structured readiness cause in its settings payload, while - `/api/ai/patrol/run` must use the `patrol_readiness_not_ready` error taxonomy - when it rejects a known-bad Patrol runtime configuration. Bounded `status`, - `cause`, `provider`, and `model` details are the canonical transport shape. - `/api/ai/patrol/findings` is the canonical Patrol-page findings source, and - `/api/ai/patrol/runs` records may expose bounded `source` provenance so demo - evidence can be separated from live runtime assessment state without - changing the stable run payload shape. - The Patrol status payload's `trigger_status` object is the canonical - transport for alert/anomaly trigger queue and mode facts. Patrol summary UI - may present that state as factual trigger-mode context, but it must not infer - trigger enablement or queue state from settings payloads, route state, or - run-history labels. -7. `frontend-modern/src/api/rbac.ts` shared with `organization-settings`: the RBAC frontend client is both an organization settings control surface and a canonical API payload contract boundary. -8. `frontend-modern/src/api/security.ts` shared with `security-privacy`: the security frontend client is both a security/privacy control surface and a canonical API payload contract boundary. -9. `frontend-modern/src/api/updates.ts` shared with `deployment-installability`: the updates frontend client is both a deployment-installability control surface and a canonical API payload contract boundary. - It must preserve `/api/updates/plan.readiness` payloads as backend-owned - API state so settings UI can render `ready`, `attention`, and `blocked` - update checks without rebuilding upgrade state locally. The frontend may - disable blocked installs, but the backend apply route remains authoritative - and must reject a recomputed `blocked` readiness verdict. -10. `frontend-modern/src/components/Settings/APITokenManager.tsx` shared with `security-privacy`: the API token settings surface is both a security/privacy control surface and a canonical API payload contract boundary. +1. `cmd/pulse-mcp/main.go` shared with `ai-runtime`: the Pulse MCP adapter runtime is both an AI runtime surface for external-agent access to Pulse Intelligence and a canonical API contract projection over the agent capabilities manifest and Pulse MCP surface tool contract. +2. `cmd/pulse-mcp/README.md` shared with `ai-runtime`: the Pulse MCP adapter guide is both an AI runtime surface for external-agent access to Pulse Intelligence and a canonical API contract projection over the agent capabilities manifest. + Manifest path placeholders projected by `cmd/pulse-mcp` are canonical API + path parameters, not string concatenation hints: adapter calls must use the + shared `internal/agentcapabilities` call projection helper to + percent-encode placeholder values as single path segments before dispatching + to Pulse so resource IDs and node IDs cannot change the declared route. That + same helper owns the top-level-argument fallback for JSON request bodies: + non-placeholder write arguments become the body when the caller does not + provide a nested `body` object, and internal tool-call metadata such as + approved-action grants is stripped before any public API body is built. + MCP tool descriptions and fallback tool input schemas must project the same + manifest-owned method/path, scope, request/response shape, category, + action-mode, approval-policy, and stable error-code metadata through that + shared helper rather than maintaining a second adapter-local capability + table. The manifest-owned `surfaceToolContracts` field is the Pulse MCP + tool allowlist for `tools/list` and `tools/call`, and the same shared + `ManifestSurfaceToolCapabilities` resolver must gate execution, so + streaming-only capabilities such as `subscribe_events`, or raw manifest + capabilities omitted from the Pulse MCP surface contract, cannot be invoked + through an MCP request/response tool even if a client sends their name + manually. Adding a raw capability to `Manifest.Capabilities` is not enough + to publish an MCP tool; the Pulse MCP `surfaceToolContracts` allowlist must + opt it into that external-agent surface. A `surfaceToolContracts` entry may + narrow the operator surface's affordance posture, but it must not re-enable a + disabled surface affordance; normalized external-adapter surface summaries + must clear tool and capability names when the effective `tools` affordance is + false. MCP initialize capability + advertisement and the shared manifest-backed tool, resource, and prompt + handlers must also pass through the manifest-owned surface affordance + contract, so tools, resources, prompts, and capability metadata are exposed + only when the target surface allows that affordance and the manifest has the + backing data to satisfy it. Prompt support is proven through + `MCPManifestSurfacePromptProjectionSupported`, not a raw workflow-prompt + presence check at individual call sites. Generated MCP prompt inventory must + also pass through the same surface prompt projection gate rather than listing + raw workflow prompts directly. + MCP notification advertising, transport-event filtering, notification + method projection, and SSE-to-MCP notification bridging are also shared API + contract projections: the event kinds listed in + `capabilities.experimental.pulseNotifications.kinds`, the + `subscribe_events` manifest description, and the API SSE producer must use + the same `internal/agentcapabilities` event vocabulary rather than + adapter-local string lists. + The agent SSE subscription transport, record parser, actionable-record + filter, and MCP notification projector are part of that same shared boundary: + `cmd/pulse-mcp` and reference probes may own connection lifetime and + reconnect policy, but they must not carry their own event-stream request + headers, subscribe status handling, `event:` / `data:` scanner loops, + transport-event parser semantics, or SSE-to-MCP notification bridge. + MCP JSON-RPC method constants, JSON-RPC envelopes, request decoding, + notification response policy, shared manifest-backed and + surface-affordance-gated tool-server semantics, + shared tool-server method dispatch, initialize/tool-call/resource payloads, + notification method projection, MCP resource URI projection, + context-backed `resources/list` / `resources/read` projection, and prompt + catalog/rendering payloads belong to the MCP adapter edge. MCP must not + re-export neutral provider-call, provider-result, registry-preparation, + result-construction, result-text, result-interpretation, or direct-execution + helpers under MCP names; those helpers live under the neutral shared tool + core and are consumed directly by Assistant and adapter surfaces. The raw + `tools/call` params decode may stay in `mcp.go`, but parameter + normalization and validation belong to the neutral shared tool-call core: + tool names must be trimmed and required, argument maps must be cloned deeply + enough to detach nested JSON-like `body` maps/slices and then initialized, + and invalid params must fail before capability lookup so empty-name calls are + not reported as unknown manifest tools. + Projected-tool behavior hints are neutral API manifest projection state: + `ToolBehaviorHints` belongs in the shared projection core, while + `MCPToolAnnotations` may exist only as a protocol-facing alias at the MCP + adapter edge. + Native Assistant direct registry execution must consume the neutral + `ToolCallParams` normalization/validation contract and the shared + invalid-params, unknown-tool, and control-disabled tool result helpers from + `internal/agentcapabilities/tool_call.go` before tool lookup or handler + dispatch, rather than carrying a second name/argument path or local + entrypoint failure envelope for in-app execution. + The approved-action text projection for Pulse tool calls is part of that + same boundary: API relay execution may receive the legacy command string, + but parsing tool names, `default_api:` prefixes, and quoted arguments must + happen in `internal/agentcapabilities`, return the shared + `ToolCallParams` shape, and pass through the same normalization and + validation contract as decoded MCP `tools/call` payloads. The same helper + owns the `current_resource` handle plus governed aliases and recursive + tool-argument detector so native Assistant, API relay execution, and + MCP-compatible adapters cannot drift on unresolved attached-resource + placeholder handling. + Structured tool response envelopes, the tool-result error-code parser, + the `verification.ok` evidence parser, provider tool/call/result shape projection, + provider-result context projection for full transcript plus model-context results, + neutral provider-tool behavior hints and Pulse governance metadata, + provider-emitted streamed tool-input parsing, final provider tool-input raw + fallback handling, native Assistant provider-tool name catalog and exact/prefix matching, + provider tool-call artifact detection and streaming tool-name prefix holding, question-type/defaulting + vocabulary, native Assistant question input parsing, and tool error-code/FSM recovery vocabulary are part of that same boundary; native Assistant tool + protocol wrappers may preserve only neutral registry tool aliases and must + not expose MCP/JSON-RPC wire aliases or carry local copies of provider + tool JSON, provider call params, chat tool-call JSON, chat tool-result JSON, + provider-native tool-call artifact regexes, the blocked/failed/retryable shape, common error-code strings, recovery error-code parsing, write self-verification evidence parsing, or the + approval-required / policy-blocked marker prefixes, payload `type` values, + formatter helpers, parser helpers, or typed approval-required payload + contract used by governed Assistant tool outcomes. Chat and API consumers + may project that shared marker payload into their own event shapes, but they + must not define anonymous local approval-marker readers or discard the shared + approved-action argument map returned for replayed tool execution. MCP-shaped + provider projection aliases and structured tool-response result aliases must + not exist as helper wrappers; MCP may keep protocol wire aliases only for the + actual `tools/call` params/result envelopes while using the neutral provider, + tool-response, and direct-execution helpers by their shared names. + Assistant registry tool schemas must also behave as API-contract values: + `agentcapabilities.Tool.NormalizeCollections` owns independent copies of + structured `InputSchema` maps, required slices, enum slices, provider + input-schema maps, and JSON-schema default containers; direct provider + schema projection helpers must normalize through that same path before + emitting provider JSON, while `ToolRegistry.Register` and + `ToolRegistry.ListTools` must normalize at their respective boundaries + instead of exposing mutable caller-owned or registry-owned schema + collections. + Manifest-backed external-agent tools follow the same rule: + `agentcapabilities.CloneCapability`, `CloneCapabilities`, and + `CloneRawMessage` must detach raw manifest `inputSchema` and `outputSchema` + bytes, structured `_meta["pulse.capability"]` maps, and error-code slices + before lookup or projection results leave the shared boundary, so MCP + `tools/list` serialization and adapter post-processing cannot mutate the + fetched or canonical manifest. + Provider tool-call input normalization is owned there too: provider + responses, stored Assistant transcript calls, and API-facing chat-message + projections must clone tool input maps, nested JSON-like argument values, and + raw provider thought-signature payload bytes + through the shared `ProviderToolCall.NormalizeCollections` path rather than + reusing mutable caller maps across surfaces. + Provider result context projection is owned there as well: native Assistant, + external adapters, and future Pulse Intelligence surfaces must use the shared + projection for full transcript results and model-context results instead of + locally pairing provider tool-result structs or reimplementing the truncation + notice. + Shared tool result normalization is also owned by the neutral tool-result + core: + `agentcapabilities.ToolResult.NormalizeCollections` must initialize empty + content collections, detach content slices, and clone structuredContent maps + before native Assistant registry execution or external MCP method dispatch + returns a result envelope. + Result-text extraction, result interpretation, JSON object and array-wrapper + structuredContent projection, and `HTTPCallResponse` to shared tool-result + wrapping must live in `internal/agentcapabilities/tool_result.go`; MCP may + alias only the actual `MCPToolResult`/`MCPContent` wire envelopes and must + not keep MCP-named constructor, text, or interpretation wrappers. + Direct native execution over those shared result envelopes must use the + surface-neutral `agentcapabilities.InterpretDirectToolExecution` helper; + MCP-named direct-execution symbols are not part of the adapter contract. + Manifest fetches, agent request + construction, API-token header placement, JSON content-type posture, + capability HTTP invocation, neutral `ExecuteCapabilityToolHTTP` + request/response execution, request/response capability body-return helpers, + and stable non-2xx error-envelope formatting are + shared-core behavior. `cmd/pulse-mcp` may own stdio and retry/backoff policy, + but it must not re-declare the MCP wire/result structs or local agent HTTP + client rules that Assistant tool execution and reference clients also + consume. +3. `frontend-modern/src/api/agentCapabilities.ts` shared with `ai-runtime`: the agent capabilities frontend client is both the Pulse Intelligence external-agent manifest consumer and a canonical API payload contract boundary. +4. `frontend-modern/src/api/agentProfiles.ts` shared with `agent-lifecycle`: the agent profiles frontend client is both an agent lifecycle control surface and a canonical API payload contract boundary. +5. `frontend-modern/src/api/ai.ts` shared with `ai-runtime`: the AI frontend client is both an AI runtime control surface and a canonical API payload contract boundary. +6. `frontend-modern/src/api/aiChat.ts` shared with `ai-runtime`: the Assistant chat frontend client is both the first-party Assistant transport surface and a canonical API payload contract boundary. +7. `frontend-modern/src/api/generated/agentCapabilities.ts` shared with `ai-runtime`: the generated agent capabilities frontend types are both the Pulse Intelligence manifest TypeScript projection and a canonical API payload contract boundary. +8. `frontend-modern/src/api/nodes.ts` shared with `agent-lifecycle`: the shared Proxmox node client is both an agent lifecycle setup/install control surface and a canonical API payload contract boundary. +9. `frontend-modern/src/api/notifications.ts` shared with `notifications`: the notifications frontend client is both a notification delivery control surface and a canonical API payload contract boundary. +10. `frontend-modern/src/api/orgs.ts` shared with `organization-settings`: the organization frontend client is both an organization settings control surface and a canonical API payload contract boundary. +11. `frontend-modern/src/api/patrol.ts` shared with `ai-runtime`: the Patrol frontend client is both an AI runtime control surface and a canonical API payload contract boundary. + The Patrol status payload owns Patrol readiness as structured API state: + provider/model/settings/tool prerequisites must travel as bounded readiness + checks instead of frontend-only heuristics or generic analysis-failed text. + `/api/settings/ai/update` must persist recoverable provider/model changes + and return the same structured readiness cause in its settings payload, while + `/api/ai/patrol/run` must use the `patrol_readiness_not_ready` error taxonomy + when it rejects a known-bad Patrol runtime configuration. Bounded `status`, + `cause`, `provider`, and `model` details are the canonical transport shape. + Patrol may demote the shared model catalog behind an explicit advanced UI + action, but it must still source selectable model routes from the shared AI + runtime catalog/settings payloads rather than introducing a Patrol-local + model list or inferred provider catalog. + `/api/ai/patrol/findings` is the canonical Patrol-page findings source, and + `/api/ai/patrol/runs` records may expose bounded `source` provenance so demo + evidence can be separated from live runtime assessment state without + changing the stable run payload shape. + The Patrol status payload's `trigger_status` object is the canonical + transport for alert/anomaly trigger queue and mode facts. Patrol summary UI + may present that state as factual trigger-mode context, but it must not infer + trigger enablement or queue state from settings payloads, route state, or + run-history labels. +12. `frontend-modern/src/api/rbac.ts` shared with `organization-settings`: the RBAC frontend client is both an organization settings control surface and a canonical API payload contract boundary. +13. `frontend-modern/src/api/security.ts` shared with `security-privacy`: the security frontend client is both a security/privacy control surface and a canonical API payload contract boundary. +14. `frontend-modern/src/api/updates.ts` shared with `deployment-installability`: the updates frontend client is both a deployment-installability control surface and a canonical API payload contract boundary. + It must preserve `/api/updates/plan.readiness` payloads as backend-owned + API state so settings UI can render `ready`, `attention`, and `blocked` + update checks without rebuilding upgrade state locally. The frontend may + disable blocked installs, but the backend apply route remains authoritative + and must reject a recomputed `blocked` readiness verdict. +15. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` shared with `ai-runtime`, `frontend-primitives`: the External agents settings panel is the optional settings-shell projection of Pulse MCP onboarding, the AI runtime connected-agent onboarding surface, and a presentation consumer of the shared agent capabilities frontend client. + The panel may own settings placement, copy composition, local fallback + rendering, and the user-facing hierarchy that frames Patrol as the primary + governed operator with MCP clients as secondary access to the same approval + policy and action history. Capability category order, labels, + descriptions, required scopes, the Pulse Intelligence surface contract, + surface affordance badges, and capability rows must still come through + `frontend-modern/src/api/agentCapabilities.ts` instead of a browser-local + category table or component-local manifest fetcher. + Manifest-owned prompt, scope, failure-code, tool-contract posture, and raw capability + inventories should be available for client builders but must stay demoted + behind a builder-oriented Developer details disclosure rather than occupying + the default Assistant settings view. The external-agent setup checklist must + stay behind a user-triggered `Show connector setup` disclosure, while direct setup + links open that disclosure automatically. Posture and policy summaries + belong under Patrol access model, while prompt, scope, failure-code, and + tool inventories must be nested behind Live manifest details so opening the + developer disclosure does not immediately dump the full manifest. Visible + nested labels should use user-facing terms such as Agent starting points and + Agent capabilities rather than raw workflow/MCP inventory labels. External + adapter posture must read as `External agents expose ... through Patrol + mode`; `Pulse MCP` and `pulse_operations_loop` may remain only as manifest + or wire names inside builder/debugging detail, not as the visible product + model a user must learn. + The External agents Patrol handoff must use `Choose Patrol mode` and + frame the first step as choosing what Patrol may handle automatically before + a connected agent can request work; connected-agent setup must not revive + activation-loop or raw MCP readiness language as the primary call to action. + When the setup checklist is expanded, Step 1 owns the visible Patrol-mode + handoff and the panel header must not repeat the same call to action. + The same wording applies when API-owned projections feed Patrol availability + or plan-comparison presentation: customer-facing helpers must describe + governed scope as what Patrol may handle automatically, not as how far Patrol + can go or how much control Patrol has. + Client config follows the same product hierarchy: it may expose + manifest-derived OpenCode and Claude-style wrappers, but the default panel + must lead with Patrol control and scoped-token setup, not raw JSON blocks. + Direct `/settings/pulse-intelligence/assistant#external-agent-setup` links + must scroll and briefly focus the External agents panel after the + Assistant settings layout settles, so the external-agent setup route lands + on the optional client setup boundary rather than generic token inventory. + Legacy `/settings/security/api#external-agent-setup` and + `/settings/security/api#pulse-mcp-setup` links must remain accepted and + redirect to the canonical Pulse Intelligence Assistant route. Normal API + Access visits keep token management first; token-preset links may still + land on API Access because credential minting remains the API token + contract, not a separate credential or execution surface. + The Patrol control handoff must target the Patrol operator surface where the + inline control level is configured, while API Access remains the token + and external-agent setup surface. + Failure-code + summaries in the panel must derive from capability `errorCodes` through the + same frontend manifest client instead of a browser-local recovery-code + registry. Explanatory capability-publication copy in that panel must point + at the manifest-owned surface contract, not raw backend capability rows, so + the API contract remains clear that Pulse MCP publication requires the + `surfaceToolContracts` boundary. Token setup handoffs in + this panel may link to the API token creation form, but the named + full-surface preset must remain the manifest-derived `Patrol external agent` + preset from `frontend-modern/src/components/Settings/APITokenManager.tsx` + and `frontend-modern/src/components/Settings/apiTokenManagerModel.ts`; the + Agent integrations panel must not hardcode or recompute the required token + scope set. MCP adapter setup facts in the panel are also manifest-owned: + server name, command, base URL flag/default, token environment variable, + supported config families, and copied OpenCode/`mcpServers` snippets must + flow through `frontend-modern/src/api/agentCapabilities.ts` from + `/api/agent/capabilities.mcpAdapter`, not component-local constants. + The default setup copy must frame connected agents as optional access to + Pulse context and Patrol work, keep Patrol as the built-in operator that + watches, acts within Patrol mode, asks when approval is required, verifies + outcomes, and records history, and say external agents use that same + boundary. It must not use "Use Pulse MCP only" or "outside client" copy, and + it must not describe external-agent setup as a separate operator journey. + The Patrol control handoff belongs to the Patrol control route; API Access + may mint the token and show MCP client setup, but it must not send operators + to Assistant settings to choose Patrol's control level. +16. `frontend-modern/src/components/Settings/APITokenManager.tsx` shared with `security-privacy`: the API token settings surface is both a security/privacy control surface and a canonical API payload contract boundary. The API token inventory table may own credential and usage cells, but it must inherit embedded table framing from `frontend-primitives` `PulseDataGrid` rather than carrying API-token-local scroll or border @@ -381,13 +915,21 @@ payload shape change when the portal presents compact client rows. Token refresh/loading state remains API contract data only while the visible spinner shell routes through frontend-primitives-owned `LoadingSpinner` instead of an API-token-local animate-spin SVG. + The create-token section may expose stable in-page anchors for sibling + API Access onboarding surfaces, but preset scope contents must continue to + flow from the API manifest client and token manager model rather than from + anchor callers such as Agent integrations. + Empty scope selection is not a wildcard request in the token manager UI: + generation must require an explicit scoped preset, custom scope, or + deliberate Full access selection before the frontend sends a create-token + payload. Token scope selector semantics stay API-contract owned, but the visible pressed/unpressed selector pill shell routes through frontend-primitives `SelectablePillButton` instead of API-token-local rounded-full selector classes. -11. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` shared with `security-privacy`: the pure API token settings model is both a security/privacy control surface and a canonical API payload contract boundary. -12. `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx` shared with `agent-lifecycle`: the inline node credential slot is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -13. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx` shared with `agent-lifecycle`: the pure infrastructure operations inventory/install model is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary. +17. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` shared with `security-privacy`: the pure API token settings model is both a security/privacy control surface and a canonical API payload contract boundary. +18. `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx` shared with `agent-lifecycle`: the inline node credential slot is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +19. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx` shared with `agent-lifecycle`: the pure infrastructure operations inventory/install model is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary. Docker and Podman install-profile labels and descriptions in that shared model must derive their customer-facing source name from `frontend-modern/src/utils/sourcePlatforms.ts` rather than page-local @@ -424,15 +966,15 @@ payload shape change when the portal presents compact client rows. Mock mode must expose authored availability targets through those same list, saved-test, and connections-ledger payloads so demo endpoints exercise the canonical API contract rather than a frontend-only fixture. -13. `frontend-modern/src/components/Settings/NodeModalAuthenticationSection.tsx` shared with `agent-lifecycle`: the node setup authentication section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -16. `frontend-modern/src/components/Settings/NodeModalBasicInfoSection.tsx` shared with `agent-lifecycle`: the node setup basic-info section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -17. `frontend-modern/src/components/Settings/nodeModalModel.ts` shared with `agent-lifecycle`: the pure node setup modal model is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -18. `frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx` shared with `agent-lifecycle`: the node setup monitoring section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -19. `frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx` shared with `agent-lifecycle`: the node setup guide section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -20. `frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx` shared with `agent-lifecycle`: the node setup status/footer section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -21. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` shared with `security-privacy`: the API token settings state hook is both a security/privacy control surface and a canonical API payload contract boundary. -22. `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts` shared with `agent-lifecycle`: the direct-node infrastructure settings state hook is both an agent lifecycle control surface and a shared Proxmox node API contract boundary. -23. `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts` shared with `agent-lifecycle`: the infrastructure discovery runtime state hook is both an agent lifecycle control surface and a shared discovery/settings API contract boundary. +20. `frontend-modern/src/components/Settings/NodeModalAuthenticationSection.tsx` shared with `agent-lifecycle`: the node setup authentication section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +21. `frontend-modern/src/components/Settings/NodeModalBasicInfoSection.tsx` shared with `agent-lifecycle`: the node setup basic-info section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +22. `frontend-modern/src/components/Settings/nodeModalModel.ts` shared with `agent-lifecycle`: the pure node setup modal model is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +23. `frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx` shared with `agent-lifecycle`: the node setup monitoring section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +24. `frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx` shared with `agent-lifecycle`: the node setup guide section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +25. `frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx` shared with `agent-lifecycle`: the node setup status/footer section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +26. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` shared with `security-privacy`: the API token settings state hook is both a security/privacy control surface and a canonical API payload contract boundary. +27. `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts` shared with `agent-lifecycle`: the direct-node infrastructure settings state hook is both an agent lifecycle control surface and a shared Proxmox node API contract boundary. +28. `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts` shared with `agent-lifecycle`: the infrastructure discovery runtime state hook is both an agent lifecycle control surface and a shared discovery/settings API contract boundary. That same shared boundary also owns settings-route polling scope for discovery payloads: the `/api/discover` refresh loop and websocket-backed discovery status hydration may run only while the operator is on the canonical Infrastructure settings workspace at `/settings/infrastructure`, not on retired infrastructure sub-routes. Discovery refreshes and scan results are route-scoped state updates. If the settings route unmounts while a request is in flight, the hook must drop the @@ -446,19 +988,23 @@ payload shape change when the portal presents compact client rows. normalize their `updated` / `timestamp` values into one millisecond-backed `lastResultAt` state so discovery review rows do not depend on transport- specific timestamp shapes. -24. `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx` shared with `agent-lifecycle`: the infrastructure install state hook is both an agent fleet lifecycle control surface and an API token, lookup, and install transport contract boundary. +29. `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx` shared with `agent-lifecycle`: the infrastructure install state hook is both an agent fleet lifecycle control surface and an API token, lookup, and install transport contract boundary. Setup-completion handoff token creation is valid only while the active infrastructure add step is an installer route (`agent`, `linux-host`, `unraid`, `docker`, or `kubernetes`). Mounting the infrastructure workspace for unrelated settings panels must not auto-create install tokens from a stale setup handoff. -25. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` shared with `agent-lifecycle`: the shared infrastructure operations state hook is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary. -26. `frontend-modern/src/components/Settings/useNodeModalState.ts` shared with `agent-lifecycle`: the node setup modal state hook is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. -27. `frontend-modern/src/constants/apiScopes.ts` shared with `security-privacy`: the API token scope catalog is both a security/privacy token-management trust surface and a canonical API token payload boundary. +30. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` shared with `agent-lifecycle`: the shared infrastructure operations state hook is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary. +31. `frontend-modern/src/components/Settings/useNodeModalState.ts` shared with `agent-lifecycle`: the node setup modal state hook is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary. +32. `frontend-modern/src/constants/apiScopes.ts` shared with `security-privacy`: the API token scope catalog is both a security/privacy token-management trust surface and a canonical API token payload boundary. Docker and Podman scope labels must use the shared source platform label rather than generic `container` copy, because those labels surface directly in token presets, custom scopes, and inventory badges. -28. `frontend-modern/src/utils/agentInstallCommand.ts` shared with `agent-lifecycle`: the shared frontend install-command helper is both an agent lifecycle control surface and a canonical API/install transport contract boundary. + The `ai:execute` scope label is customer-facing API contract copy: it must + describe Pulse Intelligence actions as governed Patrol actions for plans, + approvals, policy-allowed fixes, verification, and history rather than as + generic operations workflows or a separate agent product. +33. `frontend-modern/src/utils/agentInstallCommand.ts` shared with `agent-lifecycle`: the shared frontend install-command helper is both an agent lifecycle control surface and a canonical API/install transport contract boundary. Generated install commands are part of the API contract because they bind the UI-selected Pulse URL, token source, custom CA, insecure/plain-HTTP behavior, and `/download/pulse-agent?arch=...` availability proof into the @@ -469,11 +1015,36 @@ payload shape change when the portal presents compact client rows. PowerShell `-File` calls so Windows PowerShell argument binding cannot reinterpret copied `$true` values as strings or let preflight-only returns abort the parent wrapper before install. -29. `frontend-modern/src/utils/apiTokenPresentation.ts` shared with `security-privacy`: the API token presentation helper is both a security/privacy control surface and a canonical API token management boundary. +34. `frontend-modern/src/utils/apiTokenPresentation.ts` shared with `security-privacy`: the API token presentation helper is both a security/privacy control surface and a canonical API token management boundary. It owns the operator-facing Docker / Podman token vocabulary used by API Access, token presets, usage summaries, and revoke warnings. -30. `frontend-modern/src/utils/infrastructureSettingsPresentation.ts` shared with `agent-lifecycle`: the infrastructure settings presentation helper is both an agent lifecycle control surface and an API-backed direct-node/discovery settings boundary. -31. `internal/api/access_control_handlers.go` shared with `organization-settings`: RBAC role and user-assignment handlers are both an organization settings control surface and a canonical API payload contract boundary. +35. `frontend-modern/src/utils/infrastructureSettingsPresentation.ts` shared with `agent-lifecycle`: the infrastructure settings presentation helper is both an agent lifecycle control surface and an API-backed direct-node/discovery settings boundary. +36. `internal/agentcapabilities/action_target.go` shared with `ai-runtime`: the Pulse Intelligence governed action target type and resource-to-action-target mapping vocabulary are both the Assistant approval/runtime routing contract and the canonical API/agent target contract for governed actions. +37. `internal/agentcapabilities/control_level.go` shared with `ai-runtime`: the Pulse Intelligence control-level vocabulary and control-tool availability predicate are both the Assistant runtime gating contract and the canonical API/agent permission posture for governed action tools. +38. `internal/agentcapabilities/errors.go` shared with `ai-runtime`: the Pulse Intelligence agent error envelope is both the canonical API failure payload contract and the AI runtime adapter error-parsing contract for Assistant and external-agent surfaces. +39. `internal/agentcapabilities/events.go` shared with `ai-runtime`: the Pulse Intelligence event vocabulary is both the canonical API SSE event contract and the AI runtime adapter notification contract for Assistant and external-agent surfaces. +40. `internal/agentcapabilities/governance_prompt.go` shared with `ai-runtime`: the Pulse Intelligence surface-affordance-resolved model-facing operating-instruction, tool-governance prompt, reusable provider-tool governance description, Assistant-native offered-tool filtering, and Assistant-native interactive question-tool governance projections are both the Assistant system-prompt governance section and the shared API/agent vocabulary for action mode, approval posture, MCP affordance advertisement, and non-registry interaction-tool boundaries. +41. `internal/agentcapabilities/http.go` shared with `ai-runtime`: the Pulse Intelligence agent HTTP substrate is both the API capabilities invocation contract and the shared AI runtime adapter execution primitive for MCP and reference agent clients. +42. `internal/agentcapabilities/manifest.go` shared with `ai-runtime`: the canonical Pulse Intelligence agent capabilities manifest declaration, including capability display titles, manifest-owned finding lifecycle schemas, manifest-owned governed action schemas and routes, manifest-owned external-adapter surface tool contracts, and manifest-owned structured output schemas, is both the API discovery payload source and the AI runtime projection contract for Pulse Assistant and MCP-facing agent tools. +43. `internal/agentcapabilities/markdown.go` shared with `ai-runtime`: the Pulse Intelligence manifest Markdown projection, including manifest-owned capability titles, surface-filtered Pulse MCP tool/error inventories, and prompt labels, is both the canonical API/agent documentation projection and the AI runtime onboarding projection for Assistant-compatible external-agent surfaces. +44. `internal/agentcapabilities/mcp.go` shared with `ai-runtime`: the Pulse Intelligence MCP protocol version, JSON-RPC, method dispatch, method payload, surface-tool-contract-gated initialize operating-instruction and capability advertisement payload, manifest surface-filtered tools/list and tools/call execution bridge, manifest surface-gated resources/list and resources/read bridge, manifest-owned and surface-affordance-gated workflow prompt projection, protocol wire aliases, resource and prompt handler gates, and notification projection collectively define the external-agent adapter wire contract over the shared Pulse Intelligence tool core; MCP initialize, tools/call execution, resource list/read projection, and prompt list/get projection must enter through manifest-owned surface and workflow-prompt contracts so raw capability slices cannot bypass the published external-adapter contract. +45. `internal/agentcapabilities/mcp_adapter.go` shared with `ai-runtime`: the Pulse MCP adapter setup contract defaults and normalization are both the canonical API manifest setup projection and the AI runtime onboarding contract for Assistant-compatible external-agent surfaces. +46. `internal/agentcapabilities/projection.go` shared with `ai-runtime`: the agent capability external-tool projection helper, normalized manifest-owned surface tool contract resolution and tools-affordance gating, manifest-owned resource-context route and argument vocabulary, operator-state capability and route vocabulary, finding workflow capability and lifecycle argument vocabulary including resolution and dismissal notes, governed action capability, route, and argument vocabulary, manifest-owned tool title and outputSchema projection, structured Pulse capability _meta, and shared tool behavior hints are both the canonical API manifest projection contract and the AI runtime adapter projection for Pulse Assistant and MCP-facing agent tools, with MCP annotation and metadata wire names confined to adapter-edge aliases. +47. `internal/agentcapabilities/provider_tool_artifacts.go` shared with `ai-runtime`: the provider tool-call artifact detector and streaming tool-name prefix splitter are both the Assistant stream-sanitization boundary and the shared external-adapter leak guard for provider-native tool-call markup that escaped the structured channel. +48. `internal/agentcapabilities/schema.go` shared with `ai-runtime`: the agent capability input schema contract is both the canonical API manifest schema envelope and the AI runtime structured tool-schema, governance-aware provider-projection with neutral behavior hints and Pulse governance metadata, offered-tool governance extraction for Assistant prompt policy, manifest-affordance-gated Assistant provider-surface composition, manifest raw-schema to Assistant provider-schema projection for capability tools, legacy native Assistant utility provider aliases and schemas, provider-call normalization, provider-result context projection, Assistant-native interaction provider-tool declaration, and live Assistant execution-normalization contract for Pulse Assistant and MCP-facing agent tools. +49. `internal/agentcapabilities/scopes.go` shared with `ai-runtime`: the manifest-derived required-scope summary is both the canonical API/agent token guidance contract and the AI runtime adapter startup/onboarding contract for Assistant-compatible external-agent surfaces. +50. `internal/agentcapabilities/sse.go` shared with `ai-runtime`: the Pulse Intelligence SSE subscription transport and record parser are both the canonical API event-stream consumption contract and the AI runtime adapter push bridge contract for MCP and reference agent clients. +51. `internal/agentcapabilities/surface_contract.go` shared with `ai-runtime`: the Pulse Intelligence operator-surface affordance contract, shared surface-affordance, surface-tool identity, Assistant surface tool filtering, normalized external surface tool resolver, surface lookup, affordance labels, and manifest-published external-adapter surface tool allowlist projection are both the canonical API manifest surface model and the AI runtime prompt and onboarding guardrail for Assistant and MCP-facing surfaces. +52. `internal/agentcapabilities/text_tool_invocation.go` shared with `ai-runtime`: the Pulse Intelligence text tool invocation parser, internal approval argument, and current_resource handle vocabulary are both the Assistant approved-action execution projection and the shared tool-call params bridge for governed Pulse Intelligence tool calls, with MCP tools/call compatibility staying at the adapter edge. +53. `internal/agentcapabilities/tool_call.go` shared with `ai-runtime`: the Pulse Intelligence shared tool-call params, normalization, validation, direct registry preparation, registry-entrypoint failure result helpers, and provider/registry tool-call safety classification are both the native Assistant execution/FSM contract and the canonical API/agent tools/call compatibility contract for governed Pulse Intelligence tool calls. +54. `internal/agentcapabilities/tool_execution.go` shared with `ai-runtime`: the Pulse Intelligence neutral capability tool HTTP execution helper and direct tool execution output/error mapper are both the Assistant-native direct execution contract and the canonical API/agent request/response execution contract, with MCP adapters consuming the neutral helpers only after the shared MCP manifest-surface execution bridge has applied the published surface tool contract. +55. `internal/agentcapabilities/tool_marker.go` shared with `ai-runtime`: the Pulse Intelligence Assistant tool marker vocabulary and approval/policy marker parser are both the Assistant structured tool-result compatibility contract and the canonical API/agent branching contract for governed tool outcomes. +56. `internal/agentcapabilities/tool_names.go` shared with `ai-runtime`: the Pulse Intelligence registry tool-name vocabulary is both the native Assistant execution/display contract and the canonical API/agent tool identity contract for MCP-facing external-agent adapters. +57. `internal/agentcapabilities/tool_response.go` shared with `ai-runtime`: the shared tool response envelope, tool error-code vocabulary, and tool-result error-code and verification evidence parsers are both the Assistant structured tool-result contract and the canonical API/agent branching contract for Pulse Intelligence tool failures, recovery tracking, and write self-verification. +58. `internal/agentcapabilities/tool_result.go` shared with `ai-runtime`: the Pulse Intelligence shared tool-result content/result envelope, structuredContent projection, result constructors, HTTP response-to-result mapping, text projection, and result interpretation helpers are both the Assistant registry result contract and the canonical API/agent result projection contract for governed tool outcomes. +59. `internal/agentcapabilities/types.go` shared with `ai-runtime`: the agent capabilities manifest wire type, manifest-owned external-adapter surface tool contract field, capability display title and structured output schema fields, approval-policy vocabulary, capability governance normalization, and tool-governance descriptor shape are both the canonical API payload contract and the AI runtime projection contract for Pulse Assistant and MCP-facing agent tools. +60. `internal/agentcapabilities/workflow_prompt.go` shared with `ai-runtime`: the Pulse Intelligence workflow prompt catalogue, manifest-owned `workflowPrompts` projection, MCP prompt title projection, presentation kind hints, shared resource-context and finding argument vocabulary, Patrol issue-handling capability gating, argument validation, and manifest-gated shared prompt rendering rules are both the AI runtime starter contract for Assistant-compatible surfaces and the canonical API/agent prompt projection contract for MCP-facing clients. +61. `internal/api/access_control_handlers.go` shared with `organization-settings`: RBAC role and user-assignment handlers are both an organization settings control surface and a canonical API payload contract boundary. The shared node setup boundary above owns the guided/manual setup split for PVE/PBS consumers: API Inventory and Host Telemetry Agent setup modes are auto-registration paths, while Token ID/Value fields, Test Connection, @@ -493,7 +1064,7 @@ payload shape change when the portal presents compact client rows. controls. That sequence is presentation guidance for the existing setup payload phases; it does not create a second node setup API model or allow page-local payload ownership. -32. `internal/api/agent_install_command_shared.go` shared with `agent-lifecycle`: agent install command assembly is both an agent lifecycle control surface and a canonical API payload contract boundary. +62. `internal/api/agent_install_command_shared.go` shared with `agent-lifecycle`: agent install command assembly is both an agent lifecycle control surface and a canonical API payload contract boundary. Frontend and backend Unix install command builders must stay on the same token-file and preflight transport contract: tokens are passed to the installer as ephemeral files, and host install snippets must verify the @@ -504,12 +1075,12 @@ payload shape change when the portal presents compact client rows. the operator wants Patrol actions or server-opted-in Docker-in-LXC inventory from the Proxmox node; generic Proxmox API Inventory setup must remain least-privilege and command-execution-free. -32a. `internal/api/cloud_agent_install_command.go` shared with `agent-lifecycle`, `cloud-paid`: hosted tenant agent install commands are agent lifecycle enrollment transport, hosted/provider MSP tenant boundary, and canonical API payload contract. + 32a. `internal/api/cloud_agent_install_command.go` shared with `agent-lifecycle`, `cloud-paid`: hosted tenant agent install commands are agent lifecycle enrollment transport, hosted/provider MSP tenant boundary, and canonical API payload contract. The route and reusable helper must both mint PVE/PBS install tokens only in hosted mode, only for an existing tenant/org, and only into that tenant runtime's token store with the org boundary, command shape, token metadata, and already-loaded tenant-monitor refresh behavior preserved. -33. `internal/api/ai_handler.go` shared with `ai-runtime`: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary. +63. `internal/api/ai_handler.go` shared with `ai-runtime`: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary. Assistant session list payloads may expose only the safe `handoff_summary` projection needed by the browser to mark and restore a scoped handoff. The payload must not expose provider-bound model context, @@ -544,34 +1115,34 @@ payload shape change when the portal presents compact client rows. browser-safe `ChatSession` projection. Rename must not expose or mutate stored prompts, messages, provider reasoning, model handoff context, approvals, action state, or tool evidence. Browser clients must call the - shared `AIChatAPI.renameSession(sessionId, title)` helper so path encoding - and JSON body shape stay canonical. - `POST /api/ai/sessions/{id}/undo` and - `POST /api/ai/sessions/{id}/redo` own the Assistant turn-repair API - contract. Undo removes the latest user-authored turn and all later - assistant/tool messages as one durable unit, returns a browser-safe - `restored_prompt`, `removed_messages`, and `can_redo`, and must not expose - provider reasoning, raw tool output, model-only handoff text, approval - payload internals, or remediation command data. Redo restores the latest - undone turn and returns `restored_messages` plus the remaining `can_redo` - state. `GET /api/ai/sessions` and other `ChatSession` projections may - expose only the boolean `can_redo` hint alongside safe title/timestamp, - count, and handoff-summary fields so the drawer can re-enable redo after a - reload without reading the redo stack itself. Browser clients must use the - shared `AIChatAPI.undoLastTurn(sessionId)` and - `AIChatAPI.redoLastTurn(sessionId)` helpers so path encoding and response - shape stay canonical. - OpenCode-style file diff/revert session routes are deliberately not part - of Pulse's supported Assistant session contract: Pulse sessions do not own - local code-file edits, and infrastructure mutations must be reviewed - through governed approval/action history. Browser clients must not expose - or call `GET /api/ai/sessions/{id}/diff`, - `POST /api/ai/sessions/{id}/revert`, or - `POST /api/ai/sessions/{id}/unrevert`; legacy direct calls to those routes - return `501 Not Implemented` with an explicit unsupported message rather - than a placeholder success payload. - Resource-context follow-up turns are different from Patrol-run rehydration: - browser-safe `handoff_metadata.kind=resource_context` must not replace a + shared `AIChatAPI.renameSession(sessionId, title)` helper so path encoding + and JSON body shape stay canonical. + `POST /api/ai/sessions/{id}/undo` and + `POST /api/ai/sessions/{id}/redo` own the Assistant turn-repair API + contract. Undo removes the latest user-authored turn and all later + assistant/tool messages as one durable unit, returns a browser-safe + `restored_prompt`, `removed_messages`, and `can_redo`, and must not expose + provider reasoning, raw tool output, model-only handoff text, approval + payload internals, or remediation command data. Redo restores the latest + undone turn and returns `restored_messages` plus the remaining `can_redo` + state. `GET /api/ai/sessions` and other `ChatSession` projections may + expose only the boolean `can_redo` hint alongside safe title/timestamp, + count, and handoff-summary fields so the drawer can re-enable redo after a + reload without reading the redo stack itself. Browser clients must use the + shared `AIChatAPI.undoLastTurn(sessionId)` and + `AIChatAPI.redoLastTurn(sessionId)` helpers so path encoding and response + shape stay canonical. + OpenCode-style file diff/revert session routes are deliberately not part + of Pulse's supported Assistant session contract: Pulse sessions do not own + local code-file edits, and infrastructure mutations must be reviewed + through governed approval/action history. Browser clients must not expose + or call `GET /api/ai/sessions/{id}/diff`, + `POST /api/ai/sessions/{id}/revert`, or + `POST /api/ai/sessions/{id}/unrevert`; legacy direct calls to those routes + return `501 Not Implemented` with an explicit unsupported message rather + than a placeholder success payload. + Resource-context follow-up turns are different from Patrol-run rehydration: + browser-safe `handoff_metadata.kind=resource_context` must not replace a stored rich handoff envelope with a partial metadata-only envelope, and the handler must not ask the browser to resend resource context that the chat runtime can rehydrate from the stored selected-resource envelope. @@ -593,8 +1164,12 @@ payload shape change when the portal presents compact client rows. restored `tool_calls[].input` from backend history as a structured object as well as legacy/display strings, then normalize it before rendering. Chat stream events are generated from `internal/ai/chat` payload structs - into `frontend-modern/src/api/generated/aiChatEvents.ts`; that generated - union must not include the retired `explore_status` pre-pass event. Runtime + into `frontend-modern/src/api/generated/aiChatEvents.ts`; agent capability + manifest types are generated from `internal/agentcapabilities` payload + structs into `frontend-modern/src/api/generated/agentCapabilities.ts`. + Those generated projections must stay derived through + `scripts/generate-types.go`, and the chat event union must not include the + retired `explore_status` pre-pass event. Runtime workflow telemetry may remain a transport event, but browser Assistant streams must use `chat.StreamEvent.ClientSafe()` so raw provider `thinking` payloads and serialized tool-call prose cannot cross the API @@ -621,25 +1196,25 @@ payload shape change when the portal presents compact client rows. consumption rule only: it must not change event payload shape, event order, timeout handling, reader cleanup, parse-error handling, or ordinary content-token throughput. - Cold Assistant streams must include a typed `session` event backed by - `internal/ai/chat.SessionData` as soon as the HTTP SSE writer is ready and - an immediate neutral `workflow_state` preparation event before backend - handoff recovery, model resolution, selected-route retry planning, context - prefetch, inventory reads, or user-visible provider output so the frontend - can bind the backend-created session and show live progress without a - separate create-session request. That cold-stream model resolution must use - explicit configured chat routes or stable provider defaults without waiting - on provider model catalogs; `/api/ai/models` and settings responses own - catalog-backed recommendation, not the first-response chat stream. - The same SSE transport must emit neutral `workflow_state` events with - phase `stream_idle` during governed silent intervals while request-bound - Assistant execution is still in flight. Hidden SSE comment heartbeats are - not sufficient user-visible progress. The handler must serialize comment - heartbeats and JSON event writes through one writer lock, and the + Cold Assistant streams must include a typed `session` event backed by + `internal/ai/chat.SessionData` as soon as the HTTP SSE writer is ready and + an immediate neutral `workflow_state` preparation event before backend + handoff recovery, model resolution, selected-route retry planning, context + prefetch, inventory reads, or user-visible provider output so the frontend + can bind the backend-created session and show live progress without a + separate create-session request. That cold-stream model resolution must use + explicit configured chat routes or stable provider defaults without waiting + on provider model catalogs; `/api/ai/models` and settings responses own + catalog-backed recommendation, not the first-response chat stream. + The same SSE transport must emit neutral `workflow_state` events with + phase `stream_idle` during governed silent intervals while request-bound + Assistant execution is still in flight. Hidden SSE comment heartbeats are + not sufficient user-visible progress. The handler must serialize comment + heartbeats and JSON event writes through one writer lock, and the `stream_idle` event must stop before terminal `done`/`error` rather than becoming assistant-authored transcript content. - The generated frontend union, stream parser - tests, and backend JSON snapshot proof must stay in lockstep with that payload; + The generated frontend union, stream parser + tests, and backend JSON snapshot proof must stay in lockstep with that payload; `done.session_id` and `question.session_id` remain compatibility payloads, not the primary cold-session creation contract. Assistant `done` events must also carry the effective `model` route that completed the stream so @@ -660,48 +1235,48 @@ payload shape change when the portal presents compact client rows. fast UX iteration against the real stream reducer. That fixture may short-circuit `frontend-modern/src/api/aiChat.ts` only in Vite dev or test mode, only for reserved `/fixture ...` prompts, and only by calling the - normal `AIChatAPI.chat` event callback with the generated - `AIChatStreamEvent` union (`session`, `workflow_state`, `thinking`, - `tool_start`, `tool_progress`, `tool_cancel`, `tool_end`, `content`, - `done`). It must not open a backend session, mutate persisted chat history, - add browser-only stream event shapes, or become a production fallback for - provider or VPN failures. At least one fixture-backed Assistant tool - sequence must exercise the OpenCode-parity raw tool-input path by starting - with incomplete provider-style `raw_input`, then mutating the same tool row - with completed arguments through the normal `tool_progress` event. - At least one fixture-backed Assistant tool sequence must exercise the - skipped-tool lifecycle by resolving a visible `tool_start` row through - `tool_cancel` instead of hiding the activity. At least one fixture-backed - Assistant tool sequence must exercise live pending/running tool evidence by - emitting placeholder structured input plus raw provider-style input on - `tool_start`, mutating that same row through `tool_progress`, and pacing the - running state long enough for browser proof to inspect and copy both the - completed input and current progress before `tool_end`. At least one - fixture-backed Assistant tool sequence must exercise successful long - plain-text tool output by emitting enough output lines to prove the browser - drawer renders a bounded collapsed preview, avoids an opaque output badge, - and preserves the full output in expandable details. At least one - fixture-backed Assistant tool sequence must exercise compacted provider artifact - suppression by emitting compacted pre-tool content before a governed tool row - and final normal answer content. At least one fixture-backed Assistant tool - sequence must exercise a buffered `tool_start` -> `tool_end` burst with no - provider delay between the two events, followed by a paced content/terminal - step, so browser proof can verify that fast command activity remains - perceptible without a real provider request. At least one fixture-backed - Assistant tool sequence must exercise consecutive multi-tool activity with a - paced completed first row followed by a live second row, so browser proof can - verify replacement/compaction motion without provider timing. At least one - fixture-backed Assistant sequence must exercise `provider_retry` with `attempt`, - `max_attempts`, and `retry_after_ms` metadata plus a paced retry window, so - browser proof can verify visible retry countdown behavior without a real - provider request, VPN dependency, or API spend. At least one fixture-backed - Assistant sequence must exercise `stream_idle` after selected-provider - startup, so browser proof can verify visible idle liveness without forcing a - real provider to pause on demand. At least one fixture-backed Assistant - sequence must exercise the local prompt-send state by emitting `session`, - then pacing the first backend `workflow_state` long enough for browser proof - to verify immediate visible activity without opening a provider request. -34. `internal/api/ai_handlers.go` shared with `ai-runtime`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary. + normal `AIChatAPI.chat` event callback with the generated + `AIChatStreamEvent` union (`session`, `workflow_state`, `thinking`, + `tool_start`, `tool_progress`, `tool_cancel`, `tool_end`, `content`, + `done`). It must not open a backend session, mutate persisted chat history, + add browser-only stream event shapes, or become a production fallback for + provider or VPN failures. At least one fixture-backed Assistant tool + sequence must exercise the OpenCode-parity raw tool-input path by starting + with incomplete provider-style `raw_input`, then mutating the same tool row + with completed arguments through the normal `tool_progress` event. + At least one fixture-backed Assistant tool sequence must exercise the + skipped-tool lifecycle by resolving a visible `tool_start` row through + `tool_cancel` instead of hiding the activity. At least one fixture-backed + Assistant tool sequence must exercise live pending/running tool evidence by + emitting placeholder structured input plus raw provider-style input on + `tool_start`, mutating that same row through `tool_progress`, and pacing the + running state long enough for browser proof to inspect and copy both the + completed input and current progress before `tool_end`. At least one + fixture-backed Assistant tool sequence must exercise successful long + plain-text tool output by emitting enough output lines to prove the browser + drawer renders a bounded collapsed preview, avoids an opaque output badge, + and preserves the full output in expandable details. At least one + fixture-backed Assistant tool sequence must exercise compacted provider artifact + suppression by emitting compacted pre-tool content before a governed tool row + and final normal answer content. At least one fixture-backed Assistant tool + sequence must exercise a buffered `tool_start` -> `tool_end` burst with no + provider delay between the two events, followed by a paced content/terminal + step, so browser proof can verify that fast command activity remains + perceptible without a real provider request. At least one fixture-backed + Assistant tool sequence must exercise consecutive multi-tool activity with a + paced completed first row followed by a live second row, so browser proof can + verify replacement/compaction motion without provider timing. At least one + fixture-backed Assistant sequence must exercise `provider_retry` with `attempt`, + `max_attempts`, and `retry_after_ms` metadata plus a paced retry window, so + browser proof can verify visible retry countdown behavior without a real + provider request, VPN dependency, or API spend. At least one fixture-backed + Assistant sequence must exercise `stream_idle` after selected-provider + startup, so browser proof can verify visible idle liveness without forcing a + real provider to pause on demand. At least one fixture-backed Assistant + sequence must exercise the local prompt-send state by emitting `session`, + then pacing the first backend `workflow_state` long enough for browser proof + to verify immediate visible activity without opening a provider request. +64. `internal/api/ai_handlers.go` shared with `ai-runtime`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary. The AI settings payload on `/api/settings/ai` carries no cloud-context-privacy field: cloud context behavior is a fixed posture (real context to cloud, with credentials and local-only resources always withheld), not a settings-payload @@ -734,8 +1309,8 @@ payload shape change when the portal presents compact client rows. evidence. The frontend API client, settings shell, and Assistant drawer must treat this payload as the canonical provider health contract rather than parsing free-form provider error strings. -35. `internal/api/ai_intelligence_handlers.go` shared with `ai-runtime`: AI intelligence handlers are both an AI runtime control surface and a canonical API payload contract boundary. -36. `internal/api/config_setup_handlers.go` shared with `agent-lifecycle`: auto-register and setup handlers are both an agent lifecycle control surface and a canonical API payload contract boundary. +65. `internal/api/ai_intelligence_handlers.go` shared with `ai-runtime`: AI intelligence handlers are both an AI runtime control surface and a canonical API payload contract boundary. +66. `internal/api/config_setup_handlers.go` shared with `agent-lifecycle`: auto-register and setup handlers are both an agent lifecycle control surface and a canonical API payload contract boundary. That same shared boundary also owns reachable-host selection truth for canonical Proxmox registration: runtime callers may propose ordered `candidateHosts`, but the API contract must persist and echo the first candidate Pulse can actually reach instead of freezing the caller's rejected first preference into the stored node endpoint. That same canonical payload contract also owns strict-TLS truth for that selected host: `/api/auto-register` may only persist `VerifySSL=true` when Pulse actually captured a certificate fingerprint for the selected candidate, and it must not pretend public-CA verification is safe after every candidate fingerprint probe failed. For PVE cluster sources, that same contract must distinguish primary @@ -764,9 +1339,9 @@ payload shape change when the portal presents compact client rows. id. PBS scripts must grant `Audit` to both `pulse-monitor@pbs` and the concrete token id. Browser/runtime setup callers must not fork this into token-shared, token-unprivileged, or user-only ACL variants. -37. `internal/api/enterprise_extension_rbac_admin.go` shared with `organization-settings`: RBAC admin extension endpoints are both an organization settings control surface and a canonical API payload contract boundary. -38. `internal/api/licensing_bridge.go` shared with `cloud-paid`: commercial licensing bridge handlers carry both API payload contract and cloud-paid entitlement boundary ownership. -39. `internal/api/licensing_handlers.go` shared with `cloud-paid`: commercial licensing handlers carry both API payload contract and cloud-paid entitlement boundary ownership. +67. `internal/api/enterprise_extension_rbac_admin.go` shared with `organization-settings`: RBAC admin extension endpoints are both an organization settings control surface and a canonical API payload contract boundary. +68. `internal/api/licensing_bridge.go` shared with `cloud-paid`: commercial licensing bridge handlers carry both API payload contract and cloud-paid entitlement boundary ownership. +69. `internal/api/licensing_handlers.go` shared with `cloud-paid`: commercial licensing handlers carry both API payload contract and cloud-paid entitlement boundary ownership. That same shared licensing boundary also owns authenticated install-version and runtime-build attribution: `internal/api/router.go` must hand the canonical process `serverVersion` and normalized runtime @@ -786,20 +1361,20 @@ payload shape change when the portal presents compact client rows. destination, but the browser/API contract must not reintroduce Pulse-Pro-as-page-name copy in callback titles, actions, or retry guidance. -40. `internal/api/licensing_legacy_retry.go` shared with `cloud-paid`: the background legacy-exchange retry loop carries both API payload contract and cloud-paid entitlement boundary ownership. -41. `internal/api/notifications.go` shared with `notifications`: notification handlers are both a notification delivery control surface and a canonical API payload contract boundary. -42. `internal/api/org_handlers.go` shared with `organization-settings`: organization management handlers are both an organization settings control surface and a canonical API payload contract boundary. -43. `internal/api/org_lifecycle_handlers.go` shared with `organization-settings`: organization lifecycle handlers are both an organization settings control surface and a canonical API payload contract boundary. -44. `internal/api/payments_webhook_handlers.go` shared with `cloud-paid`: commercial payment webhook handlers carry both API payload contract and cloud-paid billing boundary ownership. -45. `internal/api/public_signup_handlers.go` shared with `cloud-paid`: hosted signup handlers carry both API payload contract and cloud-paid hosted provisioning boundary ownership. +70. `internal/api/licensing_legacy_retry.go` shared with `cloud-paid`: the background legacy-exchange retry loop carries both API payload contract and cloud-paid entitlement boundary ownership. +71. `internal/api/notifications.go` shared with `notifications`: notification handlers are both a notification delivery control surface and a canonical API payload contract boundary. +72. `internal/api/org_handlers.go` shared with `organization-settings`: organization management handlers are both an organization settings control surface and a canonical API payload contract boundary. +73. `internal/api/org_lifecycle_handlers.go` shared with `organization-settings`: organization lifecycle handlers are both an organization settings control surface and a canonical API payload contract boundary. +74. `internal/api/payments_webhook_handlers.go` shared with `cloud-paid`: commercial payment webhook handlers carry both API payload contract and cloud-paid billing boundary ownership. +75. `internal/api/public_signup_handlers.go` shared with `cloud-paid`: hosted signup handlers carry both API payload contract and cloud-paid hosted provisioning boundary ownership. That same shared boundary also owns public hosted-signup response privacy: syntactically valid `/api/public/signup` requests must return one generic `202 Accepted` Pulse Account message whether provisioning/email side effects ran or were suppressed by the owner-email limiter, while invalid bodies and true server failures remain explicit. -46. `internal/api/relay_mobile_capability.go` shared with `relay-runtime`: the backend-owned Pulse Mobile relay capability inventory is both a relay runtime boundary and a canonical API payload contract surface. -47. `internal/api/resources.go` shared with `unified-resources`: the unified resource endpoint is both a backend payload contract surface and a unified-resource runtime boundary. -48. `internal/api/security.go` shared with `security-privacy`: the security handlers are both a security/privacy control surface and a canonical API payload contract boundary. +76. `internal/api/relay_mobile_capability.go` shared with `relay-runtime`: the backend-owned Pulse Mobile relay capability inventory is both a relay runtime boundary and a canonical API payload contract surface. +77. `internal/api/resources.go` shared with `unified-resources`: the unified resource endpoint is both a backend payload contract surface and a unified-resource runtime boundary. +78. `internal/api/security.go` shared with `security-privacy`: the security handlers are both a security/privacy control surface and a canonical API payload contract boundary. That same shared security/API boundary owns CSRF replacement-token concurrency. When parallel browser mutations arrive with stale or missing CSRF tokens for the same session, `internal/api/csrf_store.go` may retain @@ -807,7 +1382,7 @@ payload shape change when the portal presents compact client rows. replacement can validate its retry. Logout, password-change, and explicit session revocation must still delete the full session token set rather than leaving any retained replacement token valid. -49. `internal/api/security_tokens.go` shared with `security-privacy`: the security token handlers are both a security/privacy control surface and a canonical API payload contract boundary. +79. `internal/api/security_tokens.go` shared with `security-privacy`: the security token handlers are both a security/privacy control surface and a canonical API payload contract boundary. Token owner identity is reserved for the server-authenticated principal: shared token-minting helpers must derive `owner_user_id` from the current session or caller token and reject extension metadata that tries to @@ -823,15 +1398,20 @@ payload shape change when the portal presents compact client rows. before minting a `relay:mobile:access` credential. Community installs may receive the standard license-required response, but direct API calls must not bypass Relay entitlement by creating mobile runtime tokens. -50. `internal/api/setup_script_render.go` shared with `agent-lifecycle`, `storage-recovery`: the generated Proxmox setup-script is a shared boundary across agent lifecycle (forced-command keys, install/uninstall edits), API contracts (rendered token shape and encoded rerun URL), and storage/recovery (backup visibility grants, Pulse-managed temperature SSH keys, and SMART disk-temperature collection). -51. `internal/api/slo.go` shared with `performance-and-scalability`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary. -52. `internal/api/system_settings.go` shared with `security-privacy`: the system settings telemetry and auth controls are both a security/privacy control surface and a canonical API payload contract boundary. -53. `internal/api/unified_agent.go` shared with `agent-lifecycle`: unified agent download and installer handlers are both an agent lifecycle control surface and a canonical API payload contract boundary. +80. `internal/api/setup_script_render.go` shared with `agent-lifecycle`, `storage-recovery`: the generated Proxmox setup-script is a shared boundary across agent lifecycle (forced-command keys, install/uninstall edits), API contracts (rendered token shape and encoded rerun URL), and storage/recovery (backup visibility grants, Pulse-managed temperature SSH keys, and SMART disk-temperature collection). +81. `internal/api/slo.go` shared with `performance-and-scalability`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary. +82. `internal/api/system_settings.go` shared with `security-privacy`: the system settings telemetry and auth controls are both a security/privacy control surface and a canonical API payload contract boundary. +83. `internal/api/unified_agent.go` shared with `agent-lifecycle`: unified agent download and installer handlers are both an agent lifecycle control surface and a canonical API payload contract boundary. Development-mode missing-binary responses must report the build command for the requested normalized OS/architecture, not a hard-coded Linux target, so installer preflight failures point operators at the artifact they actually need. -54. `internal/api/updates.go` shared with `deployment-installability`: update handlers are both a deployment-installability control surface and a canonical API payload contract boundary. +84. `internal/api/updates.go` shared with `deployment-installability`: update handlers are both a deployment-installability control surface and a canonical API payload contract boundary. +85. `pkg/aicontracts/fix_execution.go` shared with `ai-runtime`: the public approved-fix execution contract is both an AI runtime approved-action boundary and a canonical API dependency contract for Patrol and enterprise auto-fix binders. +86. `pkg/aicontracts/investigation.go` shared with `ai-runtime`: the public Patrol investigation record and finding contract is both an AI runtime handoff boundary and a canonical API payload contract for Patrol, Assistant, unified findings, persistence, and audit surfaces. +87. `pkg/aicontracts/orchestrator_deps.go` shared with `ai-runtime`: the public investigation orchestrator dependency contract is both an AI runtime handoff boundary and a canonical API payload contract for Assistant and Patrol tool-call history. +88. `pkg/extensions/ai_autofix.go` shared with `ai-runtime`: the enterprise auto-fix extension dependency seam is both an AI runtime approved-action boundary and a canonical API extension contract over Assistant and Patrol execution dependencies. +89. `scripts/generate-pulse-intelligence-docs.go` shared with `ai-runtime`: the Pulse Intelligence manifest docs generator is both an AI runtime docs/onboarding projection and a canonical API contract projection over the agent capabilities manifest and Pulse MCP surface tool contract. Update-plan responses own the structured readiness verdict for server updater capability, rollback support, agent continuity, v5 agent migration transport security, and agent reporting token scope. That verdict is part @@ -843,20 +1423,20 @@ payload shape change when the portal presents compact client rows. the requested target version through the shared update-target validation path, recompute readiness from live backend state, and reject `blocked` verdicts before update execution starts. -The platform-connections API contract also owns inactive monitored-system -candidate semantics end to end. `enabled=false` on TrueNAS or VMware preview, -test, add, and update payloads must serialize through the shared ledger client -as `active:false`, and preview responses may legitimately return `no_change`, -`removes_existing`, or `removes_multiple` with empty projected-system lists -when the disabled candidate no longer contributes to a monitored-system group. -That same monitored-system grouping contract now also owns restart-safe host -report continuity at the API boundary. The removed monitored-system limit -enforcement path must not return; the API should treat a returning standalone -host report as existing grouping context when monitoring can match it to recent -persisted host continuity, so a server restart or v6 upgrade does not change -the explanatory grouping model before the live -inventory rebuild catches up. Genuinely new host identities must still return -the canonical monitored-system blocked payload. + The platform-connections API contract also owns inactive monitored-system + candidate semantics end to end. `enabled=false` on TrueNAS or VMware preview, + test, add, and update payloads must serialize through the shared ledger client + as `active:false`, and preview responses may legitimately return `no_change`, + `removes_existing`, or `removes_multiple` with empty projected-system lists + when the disabled candidate no longer contributes to a monitored-system group. + That same monitored-system grouping contract now also owns restart-safe host + report continuity at the API boundary. The removed monitored-system limit + enforcement path must not return; the API should treat a returning standalone + host report as existing grouping context when monitoring can match it to recent + persisted host continuity, so a server restart or v6 upgrade does not change + the explanatory grouping model before the live + inventory rebuild catches up. Genuinely new host identities must still return + the canonical monitored-system blocked payload. ## Extension Points @@ -887,26 +1467,26 @@ the canonical monitored-system blocked payload. 1. Add or change payload fields through handler + contract tests together 2. Update frontend API types in lockstep with backend contract changes. Websocket-backed API consumers such as `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` and `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` may read runtime context only through `frontend-modern/src/contexts/appRuntime.ts`; they must not import `frontend-modern/src/App.tsx`, because payload ownership remains in the API contract rather than the root shell. -2a. Route settings infrastructure connected-system ledgers through - `/api/connections` and `frontend-modern/src/components/Settings/useConnectionsLedger.ts` - together. The frontend ledger may retain the last fulfilled connection - snapshot while polling or manual reload is in flight, but that retention is - only a fetch lifecycle rule; it must not synthesize rows, downgrade backend - fleet state, or replace the shared connection projection with page-local - placeholders. -2b. Route agentless availability target kind changes through - `internal/api/availability_handlers.go`, - `internal/api/platform_mock_connections.go`, - `frontend-modern/src/api/availabilityTargets.ts`, and the unified-resource - availability payload together. The bounded `targetKind` vocabulary is - `machine`, `service`, and `device`; missing legacy values default to - `service`. Browser add-dialog routes may carry a `targetKind` query value - to preselect that bounded kind, but the persisted contract remains the - `AvailabilityTarget.targetKind` payload. Browser consumers must not place - availability targets into Standalone Machines from that classification; - Machines membership requires Pulse Agent resource evidence. API clients - must not infer that classification from protocol, hostname, port, or row - label. + 2a. Route settings infrastructure connected-system ledgers through + `/api/connections` and `frontend-modern/src/components/Settings/useConnectionsLedger.ts` + together. The frontend ledger may retain the last fulfilled connection + snapshot while polling or manual reload is in flight, but that retention is + only a fetch lifecycle rule; it must not synthesize rows, downgrade backend + fleet state, or replace the shared connection projection with page-local + placeholders. + 2b. Route agentless availability target kind changes through + `internal/api/availability_handlers.go`, + `internal/api/platform_mock_connections.go`, + `frontend-modern/src/api/availabilityTargets.ts`, and the unified-resource + availability payload together. The bounded `targetKind` vocabulary is + `machine`, `service`, and `device`; missing legacy values default to + `service`. Browser add-dialog routes may carry a `targetKind` query value + to preselect that bounded kind, but the persisted contract remains the + `AvailabilityTarget.targetKind` payload. Browser consumers must not place + availability targets into Standalone Machines from that classification; + Machines membership requires Pulse Agent resource evidence. API clients + must not infer that classification from protocol, hostname, port, or row + label. 3. Add dedicated contract tests for new stable payloads Unified resource type-filter and organization-share resource type additions must route through `internal/api/resources.go`, `internal/api/org_handlers.go`, @@ -915,86 +1495,90 @@ the canonical monitored-system blocked payload. by `/api/resources` filters and cross-organization share normalization only after the unified-resource contract defines the canonical resource type and payload facet. -3a. Route diagnostics payload fields and user-facing diagnostics copy through - `internal/api/diagnostics.go`, - `internal/api/diagnostics_additional_test.go`, and - `internal/api/diagnostics_memory_test.go` together. Docker and Podman - health notes emitted by diagnostics must lead with Docker / Podman module - language and route operator recovery to the Infrastructure and - Security settings surfaces rather than generic runtime family wording or - retired agent-management destinations. -3b. Route Docker / Podman management API response copy through - `internal/api/docker_agents.go`, `internal/api/docker_metadata.go`, - `frontend-modern/src/api/monitoring.ts`, and their route/client tests - together. Operator-facing responses for Docker / Podman host removal, - hide/unhide, pending uninstall, display-name, and host metadata paths must - use Docker / Podman module or host wording instead of generic container - runtime labels. -3c. Route Assistant finding handoff context changes through - `internal/api/ai_handler.go`, `internal/api/ai_handler_test.go`, and - `internal/api/contract_test.go` together. Patrol-originated handoffs must - keep `[Finding Briefing]`, `[Finding Context]`, `[Finding Lifecycle + 3a. Route diagnostics payload fields and user-facing diagnostics copy through + `internal/api/diagnostics.go`, + `internal/api/diagnostics_contract_test.go`, + `internal/api/diagnostics_additional_test.go`, and + `internal/api/diagnostics_memory_test.go` together. Docker and Podman + health notes emitted by diagnostics must lead with Docker / Podman module + language and route operator recovery to the Infrastructure and + Security settings surfaces rather than generic runtime family wording or + retired agent-management destinations. Pulse Assistant diagnostics must + expose native runtime availability as `assistantRuntimeConnected`; the + diagnostics payload must not revive legacy MCP transport fields such as + `mcpConnected` or `mcpToolCount` for the first-party Assistant surface. + 3b. Route Docker / Podman management API response copy through + `internal/api/docker_agents.go`, `internal/api/docker_metadata.go`, + `frontend-modern/src/api/monitoring.ts`, and their route/client tests + together. Operator-facing responses for Docker / Podman host removal, + hide/unhide, pending uninstall, display-name, and host metadata paths must + use Docker / Podman module or host wording instead of generic container + runtime labels. + 3c. Route Assistant finding handoff context changes through + `internal/api/ai_handler.go`, `internal/api/ai_handler_test.go`, and + `internal/api/contract_test.go` together. Patrol-originated handoffs must + keep `[Finding Briefing]`, `[Finding Context]`, `[Finding Lifecycle Context]`, structured handoff resources, related root-cause/correlation - finding context, and structured handoff actions model-only, with the - briefing summarizing latest lifecycle event and factual governed action - artifact metadata without raw command text or Pulse-authored next-step - guidance. Structured handoff action - references may use the current live Patrol investigation-fix approval for - the finding when that approval is newer than the approval ID on the durable - record, but the payload may carry only IDs, status/risk/target metadata, - request/expiry timestamps, action plan identity and expiry, safe generated - approval summaries, command counts, and fix/action references, never the - approval command payload. Patrol - remediation-plan handoffs must use the same boundary for model-only - context: plan status, risk, step labels, and command counts are allowed, - while raw command and rollback command payloads remain in governed action - surfaces. Frontend Patrol finding-discussion handoffs must force a - request-local approval-required Assistant mode instead of inheriting the - user's persistent autonomous control setting; live approval, action artifact, - fix-outcome, and remediation-plan references only add structured action - metadata, they are not the trigger for the boundary. Frontend-visible Patrol - briefing payloads must stay compact and must not include suggested prompt - chips, Patrol-authored next-step recommendations, or route-owned - recommendation metadata. Frontend queued-fix recovery handoffs - where the live approval or action artifact payload is unavailable must still - carry that Patrol-owned finding briefing, current `fix_queued` posture, - request-local approval-required mode, and model-only evidence context; they must - not degrade into generic Assistant investigation chat or imply that - execution can proceed from missing command payloads. Expired-approval - recovery handoffs may use a still-available structured action artifact payload - only as safe metadata: description, target, risk, rationale, destructive - posture, and command count may enter the briefing, while raw command text - remains owned by governed remediation or approval surfaces. If the unified - finding list lacks a full investigation record, frontend finding-discussion - handoffs may hydrate the latest investigation session for the same safe - action artifact metadata, but they must not paste raw action command text - into the authored prompt or visible briefing. Direct - alert-investigation API handoffs through `internal/api/ai_handlers.go` must - enforce that same request-scoped boundary by setting - `ai.ExecuteRequest.AutonomousMode` to - false and `ai.ExecuteRequest.RequireCommandApproval` to true; API proof must - keep this guarded in both `internal/api/ai_handlers_test.go` and - `internal/api/contract_test.go`. Governed action artifact lines in the - briefing must derive from those - same structured action references after recovery so the briefing cannot - contradict the handoff action payload. Related finding - context must resolve from the current unified finding store, stay bounded - and deduplicated, include current recency and latest lifecycle facts, and - seed only structured handoff resources for canonical policy, state, - topology, and timeline hydration. Chat execution owns - resource-policy sanitization of the assembled model-only handoff before - prompt injection, so API payload builders may pass structured product - context without turning raw resource identity into user-authored text or - disclosure authority. -3d. Route command-agent WebSocket registration token semantics through - `internal/api/agent_exec_token_binding.go`, `internal/api/router.go`, and - `internal/api/contract_test.go` together. `agent:exec` tokens must already - be bound to the registering agent ID or hostname before `/api/agent/ws` - accepts command registration. The only first-use exception is a - Pulse-minted PVE/PBS install-command token carrying the governed install - metadata; that token may bind once to the first command agent ID and - hostname that registers, and a later different agent or hostname must be - rejected. Generic unbound `agent:exec` tokens remain fail-closed. + finding context, and structured handoff actions model-only, with the + briefing summarizing latest lifecycle event and factual governed action + artifact metadata without raw command text or Pulse-authored next-step + guidance. Structured handoff action + references may use the current live Patrol investigation-fix approval for + the finding when that approval is newer than the approval ID on the durable + record, but the payload may carry only IDs, status/risk/target metadata, + request/expiry timestamps, action plan identity and expiry, safe generated + approval summaries, command counts, and fix/action references, never the + approval command payload. Patrol + remediation-plan handoffs must use the same boundary for model-only + context: plan status, risk, step labels, and command counts are allowed, + while raw command and rollback command payloads remain in governed action + surfaces. Frontend Patrol finding-discussion handoffs must force a + request-local approval-required Assistant mode instead of inheriting the + user's persistent autonomous control setting; live approval, action artifact, + fix-outcome, and remediation-plan references only add structured action + metadata, they are not the trigger for the boundary. Frontend-visible Patrol + briefing payloads must stay compact and must not include suggested prompt + chips, Patrol-authored next-step recommendations, or route-owned + recommendation metadata. Frontend queued-fix recovery handoffs + where the live approval or action artifact payload is unavailable must still + carry that Patrol-owned finding briefing, current `fix_queued` posture, + request-local approval-required mode, and model-only evidence context; they must + not degrade into generic Assistant investigation chat or imply that + execution can proceed from missing command payloads. Expired-approval + recovery handoffs may use a still-available structured action artifact payload + only as safe metadata: description, target, risk, rationale, destructive + posture, and command count may enter the briefing, while raw command text + remains owned by governed remediation or approval surfaces. If the unified + finding list lacks a full investigation record, frontend finding-discussion + handoffs may hydrate the latest investigation session for the same safe + action artifact metadata, but they must not paste raw action command text + into the authored prompt or visible briefing. Direct + alert-investigation API handoffs through `internal/api/ai_handlers.go` must + enforce that same request-scoped boundary by setting + `ai.ExecuteRequest.AutonomousMode` to + false and `ai.ExecuteRequest.RequireCommandApproval` to true; API proof must + keep this guarded in both `internal/api/ai_handlers_test.go` and + `internal/api/contract_test.go`. Governed action artifact lines in the + briefing must derive from those + same structured action references after recovery so the briefing cannot + contradict the handoff action payload. Related finding + context must resolve from the current unified finding store, stay bounded + and deduplicated, include current recency and latest lifecycle facts, and + seed only structured handoff resources for canonical policy, state, + topology, and timeline hydration. Chat execution owns + resource-policy sanitization of the assembled model-only handoff before + prompt injection, so API payload builders may pass structured product + context without turning raw resource identity into user-authored text or + disclosure authority. + 3d. Route command-agent WebSocket registration token semantics through + `internal/api/agent_exec_token_binding.go`, `internal/api/router.go`, and + `internal/api/contract_test.go` together. `agent:exec` tokens must already + be bound to the registering agent ID or hostname before `/api/agent/ws` + accepts command registration. The only first-use exception is a + Pulse-minted PVE/PBS install-command token carrying the governed install + metadata; that token may bind once to the first command agent ID and + hostname that registers, and a later different agent or hostname must be + rejected. Generic unbound `agent:exec` tokens remain fail-closed. 4. Route unified resource sensitivity, routing, and `aiSafeSummary` payload changes through `internal/api/resources.go`, `internal/api/contract_test.go`, and the canonical frontend resource consumer proofs together; resource governance metadata must not ship as an API-only or frontend-only heuristic That same resource payload contract owns `aggregations.policyPosture` on `/api/resources` and `/api/resources/stats`. The aggregation must be derived @@ -1079,7 +1663,12 @@ the canonical monitored-system blocked payload. current action state from action audit. Backend `/api/ai/chat` refresh of a finding handoff must also recover that requester identity from the live approval record when action audit has not yet hydrated the current action - state. + state. Denying a Patrol investigation-fix approval through + `/api/ai/approvals/{id}/deny` is part of the same API contract: it must + persist the approval-store denial, record a `rejected` action-audit decision + when the approval carries a governed action plan, and update the owning + finding's investigation outcome to `fix_rejected` instead of leaving + `fix_queued` with no live approval. Action execution is API-owned as the next explicit contract: `POST /api/actions/{id}/execute` may only start execution for an approved action or an approval-free executable plan, must atomically persist the @@ -1141,20 +1730,108 @@ the canonical monitored-system blocked payload. hybrid `agent` resource with merged source facets, not as duplicate rows that disappear only after REST reconciliation. 8. Route unified-agent installer and binary download headers through `internal/api/unified_agent.go` and `internal/api/contract_test.go` together. Unified-agent BINARY downloads must keep the canonical `X-Checksum-Sha256` plus `X-Signature-Ed25519` contract for updater clients whether the binary is served locally or proxied from the matching GitHub release, instead of leaving callers to infer trust from source location alone. The served install-script endpoints (GET /install.sh and /install.ps1) are governed differently and have NO GitHub fallback at all: they serve the locally bundled AGENT installer or fail closed with 503. The agent installer is a per-build artifact bundled into every release tarball and Docker image, not a release asset, so the endpoint must never fetch the top-level GitHub install.sh release asset (the SERVER installer, which rejects the agent wizard's --url / --token-file, issue #1470). It attaches the base64-encoded `X-Signature-SSHSIG` header when the local detached signatures are present and omits it when they are not; a present-but-unsigned local agent installer is still served, because the agent install path (curl piped into bash) does not verify these headers, so correctness of the served script outranks signature presence. -9. Route canonical AI intelligence summary and resource-intelligence reads through `frontend-modern/src/api/ai.ts`, `frontend-modern/src/stores/aiIntelligence.ts`, `frontend-modern/src/stores/aiIntelligenceSummaryModel.ts`, `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`, `frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx`, the Patrol-owned section files under `frontend-modern/src/features/patrol/`, `frontend-modern/src/pages/AIIntelligence.tsx`, `internal/api/ai_handlers.go`, and `internal/api/contract_test.go` together so the summary card, store normalization owner, runtime hook, feature shell, section owners, route shell, and backend payload stay aligned on one governed surface, including the canonical recent-changes slice - while keeping the learning counters backend-only coverage, so the summary page keeps Patrol health and findings primary and renders timeline, correlation, and policy-posture data as secondary investigation context rather than as a separate headline product metric +9. Route canonical AI intelligence summary and resource-intelligence reads through `frontend-modern/src/api/ai.ts`, `frontend-modern/src/stores/aiIntelligence.ts`, `frontend-modern/src/stores/aiIntelligenceSummaryModel.ts`, `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`, `frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx`, the Patrol-owned section files under `frontend-modern/src/features/patrol/`, `frontend-modern/src/pages/AIIntelligence.tsx`, `internal/api/ai_handlers.go`, and `internal/api/contract_test.go` together so the store normalization owner, runtime hook, feature shell, Current work workspace, section owners, route shell, and backend payload stay aligned on one governed surface, including the canonical recent-changes slice + while keeping the learning counters backend-only coverage, so Patrol keeps health and findings primary and renders timeline, correlation, and policy-posture data as selected-item investigation context rather than as a separate headline product metric + and that secondary investigation context remains explanatory API evidence, + not a default workspace mode: the Patrol page may expand recent-change, + correlation, and policy-posture details only for an active Patrol finding or + an explicitly selected run record, and must not surface those backend + context payloads as a page-level forensic block from degraded summary health + alone + and the Patrol status presentation boundary, so trigger/scheduling status + from Patrol status APIs stays in the header/control surface instead of being + repeated as default status chrome, watch-only presentation says Patrol watches + infrastructure and shows current issues while the selected Watch only mode + describes the capability positively, such as Patrol watching infrastructure + and reporting issues only, avoiding repeated header/control + sentences, `Will not` policy-list wording, secondary Limits disclosures, or repeated + infrastructure-unchanged caveats, and the browser-visible label stays + operator-readable even when internal Assistant handoff metadata still uses + assessment terminology and the Patrol findings empty-state behavior, so `0 active findings` only renders as a healthy frontend conclusion when the same governed AI summary contract still reports healthy overall health; degraded or not-fully-verified health predictions must flow through to the Patrol findings surface instead of being replaced by page-local "looks healthy" copy - and the Patrol assessment headline plus compact summary-strip behavior, so the same governed AI summary contract decides whether the page leads with verified health, issues detected, coverage incomplete, or another attention state instead of letting count-only page fragments emit a stale `No issues found` conclusion - and the Patrol summary shell treatment itself, so the same governed summary contract still lands inside the shared neutral page-card base while severity travels through compact header accents and icon badges instead of a page-local full-width semantic background - and the Patrol summary/workspace badge treatment, so the API-owned finding, + and the Patrol control presentation boundary, so the always-visible Patrol + control selector owns the selected autonomy level, plan-locked installs keep + watch-only as the current capability while disabled paid-level buttons may + stay visible. Compact Pro badges and a `Plans & Billing` action may appear + only when upgrade prompts are allowed; that billing action stays a compact + handoff beside the selector rather than a separate upsell panel, and the + selector must still avoid Limits controls, hard-limit matrices, or + absent-feature explainers. + Compact control labels remain understandable decisions such as `Ask first`, + `Safe auto-fix`, and + `Autopilot` rather than transport shorthand or Pro-matrix labels, the Patrol + header description derives from the same + effective control state, and the advanced settings + drawer stays limited to model, schedule, trigger, and user-level model checks + sourced from the existing settings APIs rather than duplicating a second + control-level chooser. Runtime or provider setup + readiness may suppress run, schedule, model, trigger, and provider-repair + controls until Patrol can check infrastructure reliably, but it must not hide + the Patrol mode selector or replace it with setup-first proof/status chrome. + When provider/model readiness blocks manual Patrol while the header still + shows current infrastructure work, the primary run control projects that API + state as a direct Provider & Models setup action instead of an inert disabled + `Run Patrol` button + and the first-party Patrol mode starter boundary, so successful direct + autonomy saves may record the coarse content-free `patrol_control` workflow + prompt marker only when the effective control level or full-control + acknowledgement changes and the paid Patrol control feature is available, + while the autonomy settings API remains the source of truth for persisted + control state rather than carrying prompt content, finding context, or + completed-work proof; after recording that marker, Patrol state must reload + Patrol status, findings, approvals, and run history so first-party control + changes become visible as the next current-work step without waiting for polling + and the Patrol trust-history evidence carried by Patrol status, so historical + regressions keep the current findings empty state out of green all-clear copy + even when the active finding count is zero and the latest summary score is + otherwise healthy + and the Patrol Current work assessment behavior, so the same governed AI summary contract decides whether the workspace leads with verified health, issues detected, coverage incomplete, or another attention state instead of letting count-only page fragments emit a stale `No issues found` conclusion + and the Patrol Current work copy boundary, so the API-owned health, + finding, run, and control facts remain semantic inputs while browser-visible + empty and descriptive text stays operator-facing: what will appear there, + what Patrol may do under the selected control level, and what to run or + review next, not activation-loop proof, queue internals, or verification + accounting + and the Patrol control dialog/cross-surface Assistant handoff copy, so API + compatibility identifiers such as `patrol_configuration_failure` may remain + stable while user-facing and model-facing labels describe setting, saving, + and reviewing Patrol control rather than a generic configuration/apply flow + and the Patrol main-surface treatment itself, so the same governed summary contract lands inside the existing workspace and Current work surfaces while severity travels through compact header accents and icon badges instead of a page-local full-width verdict strip + and the Patrol workspace badge treatment, so the API-owned finding, runtime, and run-history counts remain semantic input only while visible state and count badges route through frontend-primitives-owned `StatusIndicatorBadge` and `MetadataBadge` instead of page-local class strings in the Patrol section owners - and Patrol loading indicator state remains API data only while the visible - header and finding loading spinners route through frontend-primitives-owned - `LoadingSpinner` instead of encoding API state in page-local spinner shells - and the Patrol verification summary derived from run history, so the page also states whether recent Patrol evidence came from a successful full patrol or only from scoped/erroring runs instead of leaving verification scope implicit + and the Patrol control-level copy, so frontend section owners treat + `PatrolAutonomyLevel` plus lock state as the API input for current-work and + findings-workspace descriptions instead of presenting generic investigation + or fix capabilities when the payload currently resolves to watch-only. + The same API input owns the visible Patrol control boundary summary: what + Patrol may do, what must ask for approval or Pro/runtime availability, and + any explicit hard limits must derive from `PatrolAutonomyLevel` plus lock + state, not from page-local safety thresholds or operations-loop completion + proof. The default API-backed presentation must stay on the selected control + level and its plain summary; it must not render a separate Limits disclosure, + hard-limit matrix, or always-on explanatory matrix beside the mode picker. + and the Patrol run-record copy, so `finding_ids` remains the API-owned + fail-closed scoping input while the frontend presents selected history as a + Patrol run record instead of a generic findings filter or snapshot workflow + and Patrol loading indicator state remains API data only while the visible + header and finding loading spinners route through frontend-primitives-owned + `LoadingSpinner` instead of encoding API state in page-local spinner shells + and Patrol first-load refresh failures remain degraded data state rather + than route failure, so frontend Patrol load orchestration keeps the + workspace mounted, preserves any last-known Patrol evidence, exposes a + retry affordance, and sends transport details only to debug/API diagnostics + instead of raw page copy + and the Patrol issue-row presentation boundary, so active collapsed Patrol + rows use API-owned severity, subject, recency, recurrence, and actionable + workflow state as semantic input while suppressing raw `loopState`, + investigation-status, investigation-outcome, confidence, and generic + review/detected process badges from the default row; those process details + remain available in expanded context, selected run records, Assistant + handoff context, or API diagnostics when they are actually needed + and the Patrol verification summary derived from run history, so the page also states whether recent Patrol evidence came from a successful full patrol or only from scoped/erroring runs instead of leaving verification scope implicit and the same-day activity-mix explanation derived from that governed run history, so when a recent full patrol is followed by alert-triggered or anomaly-triggered scoped work the verification surface can explain the mix directly instead of reconstructing it from page-local timing heuristics and the Patrol status recency split, so `last_patrol_at` remains reserved for completed full Patrol sweeps while scoped runs and verification checks advance `last_activity_at` without claiming a fresh full-estate verification pass and the Patrol Assistant handoff model, so frontend handoff prompts pass @@ -1162,25 +1839,53 @@ the canonical monitored-system blocked payload. bounded request metadata while leaving tool selection and remediation reasoning to the configured LLM instead of serializing a frontend-authored tool route or fix plan into the API request + and the Patrol control presentation boundary, so frontend copy may summarize + assisted mode as low or medium-risk automatic fixes allowed by policy, but + the risk threshold itself remains backend-owned and must not be inferred from + warning severity, UI labels, or page-local finding state + and the Patrol control route target, so the frontend-owned + `/patrol#patrol-control` anchor remains the canonical navigation affordance, + while `/patrol#operations-loop` remains inbound compatibility only, rather + than becoming an API payload field, Assistant prompt body, or backend + completion state machine. The canonical anchor must resolve to the visible + Patrol mode selector, not to the assessment workspace; that anchor may + route a new Pro user to Patrol mode from a generic Patrol run state, but issue-backed + progress through Assistant, governed decision, verification, and MCP parity + must still derive from real Patrol finding, investigation, approval, action, + or trust evidence owned by the Patrol state model. The native Patrol + workspace must not load the authenticated operations-loop status projection + to decide current work; that projection remains an API/MCP/telemetry and + adjacent commercial compatibility surface, while local Patrol state exposes + Patrol work from status, findings, approvals, and run history. The route + anchor itself must stay out of API payloads and must not become completion + proof. + When that projection still returns `nextAction: open_assistant` for a + current Patrol finding, the first-party Patrol page must treat it as + compatibility guidance and route the primary action into the Patrol findings + workflow; Assistant handoffs remain contextual selected-finding actions, not + the canonical API completion step for Patrol current work. and the canonical alert-triggered Patrol enqueue path in `internal/api/router.go`, so alert-fired Patrol work flows through the unified alert bridge and trigger manager instead of being duplicated by monitor callback wiring and the shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` card, so canonical recent-change timelines stay rendered through one governed frontend card instead of separate page-local list loops and the shared `frontend-modern/src/utils/resourceChangePresentation.ts` formatter used by the summary page and resource drawer, so canonical change wording does not drift across surfaces - and the Patrol-owned supporting-context selector, so same-state recent-change + and the Patrol-owned `Details` context selector, so same-state recent-change records from the canonical AI payload are normalized into changed-substate wording before Patrol renders them or attaches them to Assistant, avoiding no-op operator copy such as `online` to `online` while preserving the - backend-owned timeline event + backend-owned timeline event, and so those context payloads stay behind a + compact operator-opened affordance rather than creating a default forensic + block on the Patrol page and the `/api/ai/intelligence/changes` route plus `internal/api/contract_test.go`, so the canonical recent-changes endpoint stays on the same intelligence facade and contract snapshot instead of bypassing the shared timeline source and the canonical policy-posture snapshot derived from unified resources, so sensitivity, routing, and redaction counts stay owned by the same AI summary contract instead of being reconstructed as a page-local governance rollup and the resource-intelligence payload carried by the drawer AI card, so the resource-detail surface stays on one canonical intelligence contract instead of introducing a separate detail endpoint and the learned-correlation payload loaded into the shared AI intelligence store, so the Patrol intelligence page and the AI summary page consume the same governed correlation slice instead of each page fetching its own copy and the shared dashboard-load bundle inside `frontend-modern/src/stores/aiIntelligence.ts`, so the page orchestration stays on the store-owned bundle instead of enumerating the AI fetches inline and the Patrol page refresh lifecycle in `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`, so slow or stalled secondary reads from that shared dashboard-load bundle may continue resolving in the background while the operator-facing Patrol refresh control remains generation-aware, timeout-bounded, and reusable once Patrol findings and status are already visible + and the Patrol header support drawer in `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`, so API-owned Patrol status and trigger facts can feed a secondary Schedule & model surface without turning provider model, schedule, trigger tuning, or background-only runtime-policy pauses into the primary Patrol control decision and the shared `frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx` card, so the AI summary page renders the governed policy-posture counts while the resource drawer stays on per-resource policy lines instead of carrying duplicate posture UI loops and the dedicated `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` owner, so recent-change, learned-correlation, and policy-coverage summary text stays derived from the canonical AI payload in one place instead of as hook-local count and pluralization logic and that same Patrol investigation-context owner, so the current Patrol assessment summary may open Assistant with bounded model-only assessment, - verification, latest-run, supporting-context evidence, active-finding, and + verification, latest-run, supporting-context, active-finding, and resource reference context while leaving prioritization and next-step reasoning to the configured LLM; active finding entries may carry live pending Patrol approval posture only as safe @@ -1223,39 +1928,101 @@ the canonical monitored-system blocked payload. and the shared `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` card, so learned correlations and correlation context stay rendered through one governed frontend card instead of separate page-local list loops and the same shared correlation card's ordering and truncation rule, so callers pass raw correlations instead of encoding their own top-N sort behavior and the shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` and `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` cards' infrastructure resource-link default, so the Patrol page, resource drawer, and problem-resource dashboard panels inherit the canonical resource-filter path construction instead of rebuilding infrastructure URLs inline - and the Patrol runtime-remediation destination shared with the AI settings endpoint, so summary actions, run-history runtime-failure actions, and runtime-finding actions may reuse the governed provider-settings route while still presenting that destination to Patrol operators as Patrol provider configuration instead of generic `AI Settings` copy + and the Patrol runtime-remediation destination shared with the AI settings endpoint, so summary actions, run-history runtime-failure actions, and runtime-finding actions may reuse the governed provider-settings route while still presenting that destination in Patrol as provider configuration instead of generic `AI Settings` copy and the Patrol route-shell destination itself, so the thin page shell at `frontend-modern/src/pages/AIIntelligence.tsx` may continue to bridge the shared AI-runtime payload boundary while exposing `/patrol` as the canonical product route and keeping retired `/ai` browser entry points unregistered and the Patrol route-shell accessibility boundary, so brand icons in `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx` stay decorative when the same heading already exposes visible Patrol text, preventing duplicate accessible names such as `Pulse Patrol Patrol` - and the Patrol autonomy selector boundary, so the header composes the shared `frontend-modern/src/components/shared/FilterButtonGroup.tsx` segmented primitive for the `Monitor` / `Investigate` / `Remediate` presentation while the API contract remains the sole owner of accepted autonomy values, license-required rejection shape, and the monitor-only clamp -9. Route frontend API-client parsed error propagation, API-error-status fallback handling, allowed-status handling, custom status-specific error handling, command-trigger success envelope handling, shared response parsing pipelines, missing-resource lookup handling, metadata CRUD routing, stream event consumption, response status, collection normalization, scalar payload coercion, and structured error normalization through canonical shared helpers under `frontend-modern/src/api/` - Assistant chat stream workflow-state payloads are part of this same - frontend API-client boundary. `workflow_state` events must keep `phase`, - `message`, `state`, and `tool` stable, selected-route starts may carry - `provider` and `model`, and selected-route retry may carry `attempt`, - `max_attempts`, and `retry_after_ms`; automatic provider fallback metadata - (`provider_fallback`, `failed_provider`, `failed_model`, `next_provider`, - `next_model`) is retired from `/api/ai/chat` stream payloads. The - selected-route `provider_start` message is operator - progress copy and must be active/in-progress wording such as - ` is starting the response.` while the typed `provider` and `model` - fields carry the exact route identity. The generated - `frontend-modern/src/api/generated/aiChatEvents.ts` type must stay derived - from `internal/ai/chat/types.go` through `scripts/generate-types.go`, and - frontend API tests must pin any new generated SSE fields, including live - tool-start and tool-progress payload fields such as `phase` and `message` - and pending tool cancellation payloads such as `reason`. - That same shared org-management client boundary now owns target-consent - sharing semantics across `frontend-modern/src/api/orgs.ts`, - `internal/api/org_handlers.go`, and the shared org route wiring. Cross-org - share creation must remain a pending request until the target organization - accepts it, the payload must preserve `status`, `acceptedAt`, and - `acceptedBy`, and widening an accepted share's requested role must reset the - share back to `pending`. Downstream settings surfaces must not infer live - access from share creation alone or recreate manager-only pending-share - visibility rules locally. -10. Add or change API token scope, assignment, and revocation presentation through `frontend-modern/src/components/Settings/APITokenManager.tsx`, `frontend-modern/src/components/Settings/apiTokenManagerModel.ts`, and `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` + and the Patrol mode selector boundary, so the default header and the + Patrol mode dialog compose the shared + `frontend-modern/src/components/shared/FilterButtonGroup.tsx` primitive for + the visible `Watch only` / `Ask before changes` / `Auto-fix safe issues` / + `Policy autopilot` presentation while the API contract remains the sole owner of + accepted autonomy values, `full_mode_unlocked` persistence, + license-required rejection shape, and the monitor-only clamp; when the + API/license state clamps Patrol to monitor for plan reasons, frontend + consumers must present watch-only as the effective capability and badge paid + levels rather than rendering a default Pro-absence explainer or a full-loop + header description, while runtime-locked Pro installs may explain the missing + runtime; provider model, schedule, trigger tuning, and readiness validation + must remain advanced settings plumbing alongside that inline Patrol mode + boundary. API-adjacent frontend and docs projections may keep legacy wire + values such as `monitor`, `assisted`, and `full`, but customer-facing copy + must name the visible choices as `Watch only`, `Auto-fix safe issues`, and + `Policy autopilot` rather than leaking compatibility terminology. + policy rather than changing the compatibility API boundary; setup-only + readiness may hide those run/configuration affordances, but the visible + Patrol mode selector remains the primary policy boundary during setup + and the Patrol mode presentation boundary, so `frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx` routes active Patrol findings into the Patrol-owned findings workflow, renders the first-party loop as watch/investigate/act-under-policy/verify/record, uses `Patrol mode` as the human-facing name for the governed autonomy selector while preserving `patrol_control`, `patrolControl*`, and `patrol_autonomy` only for compatibility route and wire identifiers, and demotes Assistant and external-agent readiness out of the primary operator loop without introducing a new API request shape, frontend-authored tool route, serialized remediation plan, or page-local MCP setup contract; direct single-finding CTAs must derive from existing finding-presentation helpers and canonical routes such as the Patrol provider-settings route, selected findings, approvals, and history rows may still open contextual Assistant handoffs through their governed owners, setup-only Patrol runtime failures must use existing finding/runtime fields to render the Patrol-owned `Fix Patrol setup` framing, a dedicated setup task, and one direct `Open Provider & Models` action while suppressing the readiness banner, generic issue-row chips, filter chrome, and run-history action chrome that would compete with provider setup, but recent changes, correlations, and policy-coverage payloads must not render as a generic first-party Details/evidence console on the Patrol page, and raw finding lifecycle telemetry must be reserved for explicit all/resolved/history or selected-run review states instead of default active current-issue expansion + The Patrol page may consume server-authored readiness and preflight-backed + status, but it must translate that transport state into `Patrol setup issue` + or `Patrol setup warning`, provider/model context, and the canonical + `Open Provider & Models` action. Raw preflight, tool-call observation, and + readiness-internal wording remain API/settings diagnostics rather than + first-party Patrol operator banner copy. + and the external-agent Patrol-control status route, so `GET /api/agent/patrol-control/status` may expose only aggregate Patrol issue evidence, aggregate active issue-level Patrol finding counts, pending approvals, contextual collaboration counts inside the Assistant step, governed action counts split into approved and rejected decision counts, verified outcome counts, Patrol control starter/completed-loop/resolved-loop proof exposed through primary `patrolControl*` fields, `patrolAutonomy*` compatibility fields, `proActivationOperationsLoopStarterCount` as an older-client compatibility field, completed/resolved/value `proActivation*` compatibility aliases, optional token-backed MCP readiness, generated timestamps, and the next coarse loop action for native and MCP-facing orchestration; it must not expose finding IDs, action IDs, resource names, commands, prompt text, model output, actors, request bodies, token identity, token names, or token counts, and it must not replace the canonical fleet-context, resource-context, finding, approval, or action routes that own the underlying detail. Its `progressLabel` is operator-facing Patrol status copy: it must describe the current action or outcome in plain Patrol terms such as checking, investigating, approval, rejection, verification, and recorded history rather than activation-loop, value-proof, autonomy, or MCP-completion language. Human-facing manifest capability descriptions, generated MCP README text, and frontend projections for this route must say Patrol mode; wire identifiers such as `patrol_control`, `patrolControlCompletedOperationsLoopCount`, `patrol_autonomy`, `patrolAutonomyCompletedOperationsLoopCount`, and `patrolAutonomyValueState` may retain control/autonomy terminology for compatibility, and `proActivation*` wire identifiers may remain only as compatibility fields, not as user-visible Pro activation journey copy. The route may count generic execution lifecycle as recent loop activity, but the governance stage is decision-backed: `governedActionCount` must require approved or rejected governed-action evidence, `approvedDecisionCount` and `rejectedDecisionCount` must preserve that split without identifiers, and `verifiedOutcomeCount` must require an approved governed action with verified post-action evidence. Current active findings and pending approvals are live operator work: they must keep the next action and four-step rollup pointed at the current finding or approval before older Patrol control completed/resolved proof is presented as history. Aggregate issue evidence and resolved trust history are not by themselves current operator work, so native Patrol consumers must not turn resolved-only history into current finding copy or actions. `patrolControlCompletedOperationsLoopCount` and the mirrored `patrolAutonomyCompletedOperationsLoopCount` may only be derived when content-free Patrol control starter evidence, Patrol issue evidence, contextual Assistant or external-agent collaboration evidence, and either rejected governed-decision evidence or approved governed-decision evidence with verified outcome evidence coexist in the same status window. `patrolControlResolvedOperationsLoopCount` and the mirrored `patrolAutonomyResolvedOperationsLoopCount` may only be derived when content-free Patrol control starter evidence, Patrol issue evidence, contextual Assistant or external-agent collaboration evidence, approved governed-decision evidence, and verified outcome evidence coexist in the same status window; no Patrol control or compatibility alias field may carry prompt, account, action, resource, or finding identity. The four-step `steps` rollup must keep counts aligned with the operator evidence that satisfies each stage: Patrol counts issue evidence, Assistant counts contextual collaboration without prompt or response content, governance counts pending approvals until an approved or rejected decision exists, then counts those decisions, and verification counts verified outcomes or terminal rejected decisions when no write ran. External agents remain optional readiness context through `externalAgentReady` rather than a first-party activation gate or operator step. A rejected-only decision is terminal for the execution-verification branch because no write ran; an approved decision still requires verified outcome proof before the verification step completes. + The approved-success/verified-outcome predicate is shared inside + `internal/api/`: outbound Pulse Intelligence action telemetry and + `GET /api/agent/patrol-control/status` must both route through the same + approved-action verification helper. A completed action with + `ExecutionResult.Success=true` is execution evidence only; it must not set + `verifiedOutcomeCount`, approved-action-success telemetry, Patrol control + resolved-loop proof, or paid resolved-loop proof unless the action also has + `VerificationOutcome.Status=verified` or a canonical verification result that + ran and succeeded. +10. Route frontend API-client parsed error propagation, API-error-status fallback handling, allowed-status handling, custom status-specific error handling, command-trigger success envelope handling, shared response parsing pipelines, missing-resource lookup handling, metadata CRUD routing, stream event consumption, response status, collection normalization, scalar payload coercion, and structured error normalization through canonical shared helpers under `frontend-modern/src/api/` + Telemetry preview payloads are part of this same frontend API-client + boundary. `frontend-modern/src/api/settings.ts` must mirror the + content-free `internal/telemetry.Ping` payload shape exposed by the system + settings preview route, including Pulse Intelligence loop booleans, Assistant + and Patrol counters, operations-loop workflow starter counts, governed-action + counters including approved/rejected action decisions, and external-agent/MCP aggregate, adapter-origin, and + capability-class counters, so browser preview, privacy disclosure, and + outbound heartbeat JSON do not drift into separate contracts. + Operations-loop starter fields may count only the canonical + `pulse_operations_loop` prompt over the rotating telemetry window, with + total, native Assistant, first-party Patrol, primary Patrol control, + legacy Pro activation entry-point, and Pulse MCP counters. They are + active-journey evidence, + not proof that the user collaborated with context/tools, approved execution, + verified an outcome, or resolved a finding. + The external-agent/MCP readiness boolean in that same payload is a + manifest-surface capability signal: a non-expired API token covering any + Pulse MCP-published capability scope may configure the surface, while recent + use still requires route activity markers and generic `ai:chat` tokens do + not count as external-agent readiness. + Assistant chat stream workflow-state payloads are part of this same + frontend API-client boundary. `workflow_state` events must keep `phase`, + `message`, `state`, and `tool` stable, selected-route starts may carry + `provider` and `model`, and selected-route retry may carry `attempt`, + `max_attempts`, and `retry_after_ms`; automatic provider fallback metadata + (`provider_fallback`, `failed_provider`, `failed_model`, `next_provider`, + `next_model`) is retired from `/api/ai/chat` stream payloads. The + selected-route `provider_start` message is operator + progress copy and must be active/in-progress wording such as + ` is starting the response.` while the typed `provider` and `model` + fields carry the exact route identity. The generated + `frontend-modern/src/api/generated/aiChatEvents.ts` type must stay derived + from `internal/ai/chat/types.go` through `scripts/generate-types.go`; + `frontend-modern/src/api/generated/agentCapabilities.ts` must stay + derived from `internal/agentcapabilities`, including manifest surface + contract fields; and frontend API tests must pin + any new generated SSE fields, including live tool-start and tool-progress + payload fields such as `phase` and `message` and pending tool + cancellation payloads such as `reason`. Control-level + values exposed through AI settings or chat payloads must alias the shared + `agentcapabilities.ControlLevel` vocabulary rather than redefining + read-only/controlled/autonomous strings in API-local code. + That same shared org-management client boundary now owns target-consent + sharing semantics across `frontend-modern/src/api/orgs.ts`, + `internal/api/org_handlers.go`, and the shared org route wiring. Cross-org + share creation must remain a pending request until the target organization + accepts it, the payload must preserve `status`, `acceptedAt`, and + `acceptedBy`, and widening an accepted share's requested role must reset the + share back to `pending`. Downstream settings surfaces must not infer live + access from share creation alone or recreate manager-only pending-share + visibility rules locally. +11. Add or change API token scope, assignment, and revocation presentation through `frontend-modern/src/components/Settings/APITokenManager.tsx`, `frontend-modern/src/components/Settings/apiTokenManagerModel.ts`, and `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` That same shared token contract also owns audit scope separation: audit event, verification, summary, export, and unified action/export audit reads must require the dedicated `audit:read` scope instead of reusing broader monitoring or settings-read token grants. -11. Add or change infrastructure operations token generation, lookup, assignment, the pure unified-agent inventory/install model, the split infrastructure install state owner, the split direct-node/discovery infrastructure settings owners, the shared infrastructure-operations state provider/context shell, and install presentation through `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`, `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`, `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`, and `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`. Phase 9 retired the InfrastructureOperationsController shell and the useInfrastructureReportingState reporting path; they must not be reintroduced, and aggregator-backed reporting reads are owned by `frontend-modern/src/components/Settings/useConnectionsLedger.ts` under the frontend-primitives contract. +12. Add or change infrastructure operations token generation, lookup, assignment, the pure unified-agent inventory/install model, the split infrastructure install state owner, the split direct-node/discovery infrastructure settings owners, the shared infrastructure-operations state provider/context shell, and install presentation through `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`, `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`, `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`, and `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`. Phase 9 retired the InfrastructureOperationsController shell and the useInfrastructureReportingState reporting path; they must not be reintroduced, and aggregator-backed reporting reads are owned by `frontend-modern/src/components/Settings/useConnectionsLedger.ts` under the frontend-primitives contract. That same aggregator-backed reporting read may hide passive config or rollout handshakes from primary source-manager attention when the API reason says only that an agent has not yet reported a comparable applied @@ -1275,11 +2042,11 @@ the canonical monitored-system blocked payload. temperature-key install and uninstall edits must resolve the real authorized-keys target before filtering Pulse-managed `# pulse-` lines, and `internal/api/contract_test.go` must pin the generated shell shape. -12. Keep `internal/api/session_store.go` on a fail-closed auth-persistence boundary: persisted OIDC refresh tokens may only round-trip through encrypted-at-rest session payloads, and any missing-crypto or invalid-ciphertext path must drop the token instead of preserving plaintext-at-rest session state. -13. Keep tenant AI handler wiring on canonical provider ownership: `internal/api/ai_handlers.go` may wire tenant `ReadState` and tenant-scoped unified-resource providers into AI services, but it must not revive tenant snapshot-provider bridges once Patrol can initialize and verify from those canonical providers directly. -14. Keep Patrol status transport semantics explicit in that same AI handler layer: the Patrol status endpoint must carry machine-readable runtime availability such as blocked, running, disabled, active, or unavailable rather than asking frontend consumers to infer operator state from stale summaries or run history. -15. Keep legacy Patrol quickstart transport semantics retired from the public v6 GA contract: ordinary AI settings and Patrol status payloads must not expose quickstart credit/status fields, and any stale hosted-model blocked copy that survives from compatibility state must normalize back to provider/local-model setup rather than presenting credit badges or acquisition prompts. -16. Keep Patrol intelligence summary transport semantics single-voiced: the canonical overall-health payload and Patrol run-history payload together must support one primary assessment plus one explicit verification explanation, and frontend consumers must not need to derive a second compact assessment or verification verdict row from the same payloads beneath the primary assessment strip. +13. Keep `internal/api/session_store.go` on a fail-closed auth-persistence boundary: persisted OIDC refresh tokens may only round-trip through encrypted-at-rest session payloads, and any missing-crypto or invalid-ciphertext path must drop the token instead of preserving plaintext-at-rest session state. +14. Keep tenant AI handler wiring on canonical provider ownership: `internal/api/ai_handlers.go` may wire tenant `ReadState` and tenant-scoped unified-resource providers into AI services, but it must not revive tenant snapshot-provider bridges once Patrol can initialize and verify from those canonical providers directly. +15. Keep Patrol status transport semantics explicit in that same AI handler layer: the Patrol status endpoint must carry machine-readable runtime availability such as blocked, running, disabled, active, or unavailable rather than asking frontend consumers to infer operator state from stale summaries or run history. +16. Keep legacy Patrol quickstart transport semantics retired from the public v6 GA contract: ordinary AI settings and Patrol status payloads must not expose quickstart credit/status fields, and any stale hosted-model blocked copy that survives from compatibility state must normalize back to provider/local-model setup rather than presenting credit badges or acquisition prompts. +17. Keep Patrol intelligence summary transport semantics single-voiced: the canonical overall-health payload and Patrol run-history payload together must support one primary assessment plus one explicit verification explanation, and frontend consumers must not need to derive a second compact assessment or verification verdict row from the same payloads beneath the primary assessment strip. That same transport split now supports the visible Patrol assessment and action metadata without adding another API field: the frontend summary contract derives compact state from existing overall-health, run-history, @@ -1293,10 +2060,10 @@ the canonical monitored-system blocked payload. compact operator summary, but it must not imply a hero, card, duplicate verdict layout, or expanded assessment panel. Normal Patrol page consumers should keep explanatory assessment, verification, activity, and supporting - context in the owning Findings, Runs, and Supporting context surfaces rather + context in the owning Findings, Runs, and `Details` surfaces rather than re-expanding the primary assessment strip. -17. Keep Pulse Mobile relay credential minting and permission ownership on backend ownership: `internal/api/router_routes_auth_security.go`, `internal/api/security_tokens.go`, `internal/api/auth.go`, `internal/api/relay_mobile_capability.go`, `internal/api/router_routes_ai_relay.go`, and `frontend-modern/src/api/security.ts` may expose the canonical mobile runtime token creator and governed route gates, but browser callers must only consume that route and must not define the mobile runtime scope, compatibility gate list, route inventory, or token-purpose metadata locally. -18. Keep hosted tenant browser-session precedence on the shared auth boundary: `internal/api/auth.go`, `internal/api/contract_test.go`, and hosted tenant callers must treat a valid `pulse_session` as authoritative before any API-only token fallback or no-local-auth anonymous fallback, so cloud handoff can continue into protected hosted routes without flattening the operator back to `anonymous` or forcing a browser session through bearer-token-only mode after the tenant has minted API tokens. +18. Keep Pulse Mobile relay credential minting and permission ownership on backend ownership: `internal/api/router_routes_auth_security.go`, `internal/api/security_tokens.go`, `internal/api/auth.go`, `internal/api/relay_mobile_capability.go`, `internal/api/router_routes_ai_relay.go`, and `frontend-modern/src/api/security.ts` may expose the canonical mobile runtime token creator and governed route gates, but browser callers must only consume that route and must not define the mobile runtime scope, compatibility gate list, route inventory, or token-purpose metadata locally. +19. Keep hosted tenant browser-session precedence on the shared auth boundary: `internal/api/auth.go`, `internal/api/contract_test.go`, and hosted tenant callers must treat a valid `pulse_session` as authoritative before any API-only token fallback or no-local-auth anonymous fallback, so cloud handoff can continue into protected hosted routes without flattening the operator back to `anonymous` or forcing a browser session through bearer-token-only mode after the tenant has minted API tokens. That same shared auth boundary also owns hosted handoff authorization. `internal/api/cloud_handoff.go`, `internal/api/cloud_handoff_handlers.go`, and hosted tenant callers must derive the effective tenant role from pre-existing server-side org membership only, rather than trusting the handoff JWT to append missing members, @@ -1331,7 +2098,7 @@ the canonical monitored-system blocked payload. server-side for org metadata and RBAC assignment while using returned contact email only for `GenerateToken`/`SendMagicLink`; the accepted signup response remains uniform and must not expose the owner principal. -19. Keep tenant settings-scope authorization aligned with org management: `internal/api/security_setup_fix.go`, `internal/api/contract_test.go`, and settings-bound hosted callers must allow the current non-default org owner/admin membership to exercise privileged tenant routes, rather than requiring a separate configured local admin identity after hosted handoff. +20. Keep tenant settings-scope authorization aligned with org management: `internal/api/security_setup_fix.go`, `internal/api/contract_test.go`, and settings-bound hosted callers must allow the current non-default org owner/admin membership to exercise privileged tenant routes, rather than requiring a separate configured local admin identity after hosted handoff. Hosted handoff must not be treated as an org-management side effect for that same privilege boundary. Only canonical invitation, membership-management, or explicit owner-transfer flows may create tenant membership or change the stored owner/admin role. Shared auth routes and downstream settings consumers must treat handoff role @@ -1358,7 +2125,7 @@ the canonical monitored-system blocked payload. development workflows, but release builds must compile that env override out entirely instead of reading it and deciding at runtime whether to honor or ignore it. -20. Keep mobile onboarding payload reads aligned with the server-owned relay-mobile credential: `internal/api/router_routes_ai_relay.go`, `internal/api/onboarding_handlers.go`, and `internal/api/contract_test.go` must allow the dedicated `relay:mobile:access` scope to reach the governed QR, deep-link, and connection-validation payloads without reintroducing a broader `settings:read` requirement for token-authenticated pairing clients. +21. Keep mobile onboarding payload reads aligned with the server-owned relay-mobile credential: `internal/api/router_routes_ai_relay.go`, `internal/api/onboarding_handlers.go`, and `internal/api/contract_test.go` must allow the dedicated `relay:mobile:access` scope to reach the governed QR, deep-link, and connection-validation payloads without reintroducing a broader `settings:read` requirement for token-authenticated pairing clients. That same shared relay/runtime boundary also owns hostname target equivalence for agent command routing. `internal/api/router_routes_ai_relay.go` and `internal/api/contract_test.go` may match a short host against the @@ -1386,13 +2153,13 @@ the canonical monitored-system blocked payload. authenticated Proxmox node agent, and must keep inventory collection behind the monitoring-owned minimal Docker summary contract rather than exposing a new public API payload shape. -21. Keep hosted billing-state quickstart grants retired from new shared API flows: `internal/api/hosted_entitlement_refresh.go`, hosted signup, and trial-state construction must not auto-grant or refresh quickstart inventory for new workspaces, while low-level billing-state readers may still preserve historical fields that already exist on disk. -22. Keep hosted AI settings bootstrap on the shared API contract as a retired path: `internal/api/ai_hosted_runtime.go`, `internal/api/ai_handlers.go`, `internal/api/ai_handler.go`, and `internal/api/contract_test.go` must treat a missing `ai.enc` in hosted mode as an unconfigured BYOK/local-provider state, not as a machine-owned `quickstart:pulse-hosted` bootstrap condition. Hosted tenant reads may inherit billing state for commercial authorization, but they must not create quickstart-backed AI config or call the quickstart bootstrap upstream route. -23. Keep post-boot AI enablement contract-backed on the shared AI/mobile approval surface: `internal/api/ai_handler.go`, `internal/api/ai_handlers.go`, `internal/api/router_routes_ai_relay.go`, and `internal/api/contract_test.go` must turn the governed approvals-list API into the canonical empty-list payload as soon as settings-driven AI enablement succeeds, rather than leaving that surface on `503 Approval store not initialized` until some separate startup-only side effect happens. -24. Keep infrastructure summary chart transport contract-backed on the shared API surface: `internal/api/router.go`, `internal/api/contract_test.go`, and frontend infrastructure summary consumers must normalize long-range mixed-cadence history into equal-time summary buckets before shipping the infrastructure charts API payload, so 7-day and 30-day summary cards do not expose compressed right-edge tails just because recent samples arrive at a finer storage resolution. -25. Keep long-range workload chart transport time-proportional on the shared API surface: `internal/api/router.go`, `internal/api/contract_test.go`, and workload chart consumers must cap mixed-cadence workload history by equal-time buckets rather than raw point index for the per-workload and aggregate workload chart APIs, so 7-day and 30-day workload cards do not bunch recent samples at the right edge just because recent telemetry is stored more densely. -26. Keep chart timestamp precision canonical on that same shared API surface: when `internal/api/router.go` serializes monitoring history into infrastructure or workload chart payloads, it must preserve canonical millisecond timestamps from the shared monitoring timeline instead of rounding through whole-second conversion, so seeded mock history and live appends collapse onto one operator-visible timeline instead of appearing as duplicated tail samples. -27. Keep Patrol remediation payload naming backward-compatible without leaking +22. Keep hosted billing-state quickstart grants retired from new shared API flows: `internal/api/hosted_entitlement_refresh.go`, hosted signup, and trial-state construction must not auto-grant or refresh quickstart inventory for new workspaces, while low-level billing-state readers may still preserve historical fields that already exist on disk. +23. Keep hosted AI settings bootstrap on the shared API contract as a retired path: `internal/api/ai_hosted_runtime.go`, `internal/api/ai_handlers.go`, `internal/api/ai_handler.go`, and `internal/api/contract_test.go` must treat a missing `ai.enc` in hosted mode as an unconfigured BYOK/local-provider state, not as a machine-owned `quickstart:pulse-hosted` bootstrap condition. Hosted tenant reads may inherit billing state for commercial authorization, but they must not create quickstart-backed AI config or call the quickstart bootstrap upstream route. +24. Keep post-boot AI enablement contract-backed on the shared AI/mobile approval surface: `internal/api/ai_handler.go`, `internal/api/ai_handlers.go`, `internal/api/router_routes_ai_relay.go`, and `internal/api/contract_test.go` must turn the governed approvals-list API into the canonical empty-list payload as soon as settings-driven AI enablement succeeds, rather than leaving that surface on `503 Approval store not initialized` until some separate startup-only side effect happens. +25. Keep infrastructure summary chart transport contract-backed on the shared API surface: `internal/api/router.go`, `internal/api/contract_test.go`, and frontend infrastructure summary consumers must normalize long-range mixed-cadence history into equal-time summary buckets before shipping the infrastructure charts API payload, so 7-day and 30-day summary cards do not expose compressed right-edge tails just because recent samples arrive at a finer storage resolution. +26. Keep long-range workload chart transport time-proportional on the shared API surface: `internal/api/router.go`, `internal/api/contract_test.go`, and workload chart consumers must cap mixed-cadence workload history by equal-time buckets rather than raw point index for the per-workload and aggregate workload chart APIs, so 7-day and 30-day workload cards do not bunch recent samples at the right edge just because recent telemetry is stored more densely. +27. Keep chart timestamp precision canonical on that same shared API surface: when `internal/api/router.go` serializes monitoring history into infrastructure or workload chart payloads, it must preserve canonical millisecond timestamps from the shared monitoring timeline instead of rounding through whole-second conversion, so seeded mock history and live appends collapse onto one operator-visible timeline instead of appearing as duplicated tail samples. +28. Keep Patrol remediation payload naming backward-compatible without leaking legacy automation-first wording into product copy. `frontend-modern/src/api/patrol.ts`, `internal/api/ai_handlers.go`, and `internal/api/router_routes_ai_relay.go` may continue to expose stable transport fields such as `fixed_count`, @@ -1400,8 +2167,8 @@ the canonical monitored-system blocked payload. API license-required messages, and presentation labels layered on that API contract must describe the operator-visible capability as remediation or safe remediation workflows. -27. Keep storage chart identity canonical on that same shared API surface: the shared storage charts endpoint must key pool and physical-disk series by the resolved unified-resource `MetricsTarget.ResourceID`, not by canonical resource IDs or page-local aliases, so storage rows, focused summary cards, sticky summary shells, and detail charts all address the same history series in live and mock mode. -28. Keep synthetic summary-chart fallback identity canonical on that same shared API surface: when `internal/api/router.go` has to synthesize mock summary history for infrastructure, workloads, or storage cards, it must derive the fallback from canonical `resourceType`, `resourceID`, and `metricType` ownership instead of raw min/max seed-prefix helpers, so range changes and runtime mock updates stay on one governed timeline. +29. Keep storage chart identity canonical on that same shared API surface: the shared storage charts endpoint must key pool and physical-disk series by the resolved unified-resource `MetricsTarget.ResourceID`, not by canonical resource IDs or page-local aliases, so storage rows, focused summary cards, sticky summary shells, and detail charts all address the same history series in live and mock mode. +30. Keep synthetic summary-chart fallback identity canonical on that same shared API surface: when `internal/api/router.go` has to synthesize mock summary history for infrastructure, workloads, or storage cards, it must derive the fallback from canonical `resourceType`, `resourceID`, and `metricType` ownership instead of raw min/max seed-prefix helpers, so range changes and runtime mock updates stay on one governed timeline. The same compact chart boundary also owns aggregate-only storage summary transport. `/api/charts/storage-summary` may batch only the canonical `used` and `avail` storage series required for the aggregate capacity @@ -1411,24 +2178,24 @@ the canonical monitored-system blocked payload. When mock mode is active, that same endpoint must come from the monitor-owned aggregate summary cache rather than rehydrating each pool chart on request. -29. Keep workload-chart response identity canonical on that same shared API surface: `internal/api/router.go`, `internal/api/contract_test.go`, and workload summary consumers must emit provider-backed VM and system-container series under the same canonical workload IDs that workloads page rows use, while resolving history through the unified `MetricsTarget.ResourceID`, so hover and focus selection do not fall off for provider-backed rows. +31. Keep workload-chart response identity canonical on that same shared API surface: `internal/api/router.go`, `internal/api/contract_test.go`, and workload summary consumers must emit provider-backed VM and system-container series under the same canonical workload IDs that workloads page rows use, while resolving history through the unified `MetricsTarget.ResourceID`, so hover and focus selection do not fall off for provider-backed rows. Kubernetes pod workload rows follow that same contract through their metrics target. `/api/resources` may expose pod history only through the unified `MetricsTarget.ResourceID`, but that target must be the canonical prefixed runtime key `k8s::pod:` and not the bare source pod ID, so pod workload rows and pod chart payloads stay on one history series. -30. Keep the hosted account portal bootstrap intelligible without duplicate +32. Keep the hosted account portal bootstrap intelligible without duplicate chrome. `internal/cloudcp/portal/page.go`, the maintained portal frontend bundle, and the shared portal styles may refine layout density, but the account/billing shell must remain understandable from the primary header, section title, and factual body content alone instead of depending on a second context-chip strip to restate the same scope. -31. Keep storage wire metadata lossless across shared API payload types. +33. Keep storage wire metadata lossless across shared API payload types. `frontend-modern/src/types/api.ts` must continue to expose provider-backed storage metadata such as Proxmox `pool` and `zfsPool` fields when the backend emits them, instead of silently dropping that detail from the shared runtime contract. -32. Keep hosted entitlement refresh ownership on the same governed API contract +34. Keep hosted entitlement refresh ownership on the same governed API contract as hosted status and entitlements reads. `internal/api/licensing_handlers.go`, `internal/api/hosted_entitlement_refresh.go`, and `internal/api/contract_test.go` must resolve the effective hosted billing @@ -1441,7 +2208,7 @@ the canonical monitored-system blocked payload. must route through the `HostedEntitlement*` licensing aliases rather than treating the retired trial-activation callback as the active acquisition model. -33. Keep public demo bootstrap posture on the shared security-status contract. +35. Keep public demo bootstrap posture on the shared security-status contract. `internal/api/router_routes_auth_security.go`, `internal/api/security_status_capabilities.go`, frontend security-status consumers, and shared demo-mode stores must treat @@ -1453,7 +2220,7 @@ the canonical monitored-system blocked payload. the store boundary, so public demo shells do not probe `/api/ai/approvals` or `/api/ai/remediation/plans` after the read-only demo posture is already known. -34. Keep public demo commercial posture middleware-owned on that same shared +36. Keep public demo commercial posture middleware-owned on that same shared API contract. `internal/api/demo_middleware.go`, `internal/api/demo_mode_commercial.go`, `internal/api/subscription_entitlements.go`, and @@ -1465,7 +2232,7 @@ the canonical monitored-system blocked payload. hidden. Upgrade prompts, trial nudges, monitored-system migration guidance, usage counts, billing identity, and plan metadata must therefore not depend on hidden commercial routes surviving the public demo boundary. -35. Keep the storage summary route in `internal/api/router.go` as the +37. Keep the storage summary route in `internal/api/router.go` as the canonical storage summary contract across dashboard and storage consumers. `internal/api/router.go`, `internal/api/contract_test.go`, and shared frontend consumers must expose @@ -1473,7 +2240,7 @@ the canonical monitored-system blocked payload. metrics-target IDs, preserve millisecond chart timestamps, and avoid reconstructing storage summary behavior from per-pool `/api/metrics-store/history` fan-out. -36. Keep infrastructure summary metric filtering canonical on that same shared +38. Keep infrastructure summary metric filtering canonical on that same shared API surface. `frontend-modern/src/api/charts.ts`, `internal/api/router_routes_monitoring.go`, `internal/api/router.go`, `internal/api/types.go`, and `internal/api/contract_test.go` must route @@ -1484,7 +2251,7 @@ the canonical monitored-system blocked payload. requested metric filters through the shared guest-chart batch loader in `internal/monitoring/monitor_metrics.go` instead of fetching the full guest metric set and trimming after the API payload is already assembled. -37. Keep the retired compact dashboard overview route absent from that same +39. Keep the retired compact dashboard overview route absent from that same shared API surface. `internal/api/resources.go`, `internal/api/router_routes_monitoring.go`, and `frontend-modern/src/api/resources.ts` must not restore @@ -1493,7 +2260,7 @@ the canonical monitored-system blocked payload. rows, governed resource labels, top-infrastructure identity, or metrics- target join keys. New summary payloads must be owned by their product route and pinned in the API contract there. -38. Keep mock and demo chart reads on the same canonical unified snapshot as +40. Keep mock and demo chart reads on the same canonical unified snapshot as the rest of the API surface. `internal/api/router.go`, `internal/api/contract_test.go`, and chart consumers must route `/api/charts`, `/api/charts/infrastructure`, and `/api/storage-charts` @@ -1501,7 +2268,7 @@ the canonical monitored-system blocked payload. presentation is active, so VMware, storage, and infrastructure series stay aligned with `/api/resources` and `/api/state` instead of drifting onto the live store-backed graph. -39. Route the unified connections ledger and address probe through +41. Route the unified connections ledger and address probe through `internal/api/connections_types.go`, `internal/api/connections_aggregator.go`, `internal/api/connections_handlers.go`, @@ -1631,7 +2398,10 @@ the canonical monitored-system blocked payload. ## Completion Obligations -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 Verify Patrol 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 +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 + Patrol page must present it as setup/model-check state and must not render + raw preflight or tool-call-observation copy in the header banner. 2. Update frontend API types in the same slice 3. Route runtime changes through the explicit API-contract proof policies in `registry.json`; default fallback proof routing is not allowed 4. Update this contract when canonical payload ownership changes @@ -1643,7 +2413,9 @@ the canonical monitored-system blocked payload. `event_triggers_blocked_reason`, and `event_triggers_blocked_message` explain effective runtime policy without making the transport look like the operator turned alert or anomaly triggers - off + off; default Patrol UI surfaces may use that payload to explain an + actionable manual-run block, but must not promote a background-only policy + pause as ordinary operator guidance when manual Patrol still works and the split Patrol trigger settings contract, so `patrol_alert_triggers_enabled` and `patrol_anomaly_triggers_enabled` are the canonical AI settings fields while legacy `patrol_event_triggers_enabled` remains a compatibility aggregate rather than the primary control surface and the retired quickstart compatibility contract, so `/api/settings/ai` and `/api/ai/patrol/status` no longer carry `quickstart_credits_remaining`, @@ -1767,8 +2539,7 @@ the canonical monitored-system blocked payload. `recommendation`, and the Finding to UnifiedFinding conversion in `internal/api/router.go` must copy `f.PreviousResolvedFixSummary` alongside the other operator-facing strings. - The same shape also carries an optional `remind_at` timestamp (ISO - 8601) on both `UnifiedFindingRecord` and Patrol `Finding` shapes. It + The same shape also carries an optional `remind_at` timestamp (ISO 8601) on both `UnifiedFindingRecord` and Patrol `Finding` shapes. It is populated only when `dismissed_reason === 'will_fix_later'` and represents the wake-up deadline at which the next re-detection clears the dismissal — the operator-facing half of the canonical @@ -1782,7 +2553,7 @@ the canonical monitored-system blocked payload. time and badge dismissed-as-will_fix_later rows with the pending remind-at, otherwise the new behavior is invisible until the reminder fires. - The Patrol operator-feedback endpoint surface also includes + The Patrol manual-feedback endpoint surface also includes `POST /api/ai/patrol/resolve` (handled by `HandleResolveFinding` in `internal/api/ai_handlers.go`) for operator-driven manual resolution when the operator has fixed an issue out-of-band. The TS client @@ -1809,10 +2580,11 @@ the canonical monitored-system blocked payload. `PatrolIntelligenceHeader` shell on the Patrol API surface reads the title, description, and title tooltip from `getPatrolPageHeaderMeta()` rather than carrying inline copy, so the - operator-facing framing of what Patrol owns (scheduled probing, - context assembly for the configured model, and approval-bound action) - stays tied to the contract instead of drifting between hover and - inline. + operator-facing framing of what Patrol owns (watching infrastructure, + detecting issues, recording findings, and escalating into governed + investigation/action only when the selected Patrol mode + allows it) stays tied to the contract instead of drifting between + hover and inline. The Patrol page must not introduce header or workspace trust strips over this payload. High-signal trust facts from `state.patrolStatus()?.trust` (the `FindingsTrustSummary` block already @@ -1828,7 +2600,7 @@ the canonical monitored-system blocked payload. new API surface. The label stays optional when coverage is zero, says verified only for successful full patrols, and uses neutral checked wording for errored full patrols or scoped activity. - The primary Patrol assessment copy uses that same run-history-backed + The Patrol Current work empty-state copy uses that same run-history-backed coverage truth when reconciling AI summary coverage factors: a successful full run with non-zero `resources_checked` suppresses stale coverage-incomplete wording, while scoped, missing, zero-coverage, or errored @@ -1922,8 +2694,8 @@ the canonical monitored-system blocked payload. and must not expose raw command text or raw execution output. Frontend handoff briefings must derive from the same shared investigation payload rather than inventing a second finding-context transport shape. -7. Keep Patrol summary payload consumers aligned on one assessment hierarchy: transport-driven Patrol summary surfaces may show supporting counts and outcomes, but the canonical assessment and verification states must remain singular and not be repeated as a second compact verdict strip. The collapsed readout should describe current operator state and score directly rather than combining reassuring grade labels with active-issue copy. -8. Keep Patrol verification and activity facts unified on one transport-backed secondary status area: when frontend consumers combine Patrol status payloads (`runtime_state`, `last_patrol_at`, `last_activity_at`, `trigger_status`) with run-history transport, the latest run result, activity mix, scoped-trigger state, and circuit-breaker context must read as one supporting explanation beneath the primary assessment instead of being re-expanded into a separate full-width status strip plus duplicate summary layers +7. Keep Patrol summary payload consumers aligned on one assessment hierarchy: transport-driven Patrol summary data may show supporting counts and outcomes, but the canonical assessment and verification states must remain singular and not be repeated as a second compact verdict strip. The collapsed readout should describe current operator state and score directly rather than combining reassuring grade labels with active-issue copy; recurrence and trust counters are historical evidence unless the active finding or approval payload marks current operator work. Patrol-owned status, finding, and run-history payloads are sufficient fallback evidence for Current issues when the broader intelligence summary payload is missing, slow, or unavailable. Frontend consumers of the operations-loop projection must present the compact progress label as current work state, not as a duplicate of the selected Patrol autonomy mode, and terminal verified/rejected outcomes with no active finding or pending approval must stay history detail behind `No active work`; native Patrol consumers must not render a separate proof strip solely to expose that history action. A first-party Patrol control starter may render a ready-to-run Patrol step only after subtracting the legacy `proActivationOperationsLoopStarterCount` alias from aggregate Patrol-control starter evidence, so a legacy activation-entry count alone cannot revive the old proof UI. +8. Keep Patrol verification and activity facts unified on one transport-backed secondary status area: when frontend consumers combine Patrol status payloads (`runtime_state`, `last_patrol_at`, `last_activity_at`, `trigger_status`) with run-history transport, the latest run result, activity mix, scoped-trigger state, and circuit-breaker context must read as one supporting explanation beneath Current issues or selected history instead of being re-expanded into a separate full-width status strip plus duplicate summary layers and the Patrol runtime-failure run-history contract, so backend payloads, persistence adapters, and `frontend-modern/src/api/patrol.ts` preserve `error_summary` and `error_detail` whenever an erroring run has structured @@ -1936,19 +2708,15 @@ the canonical monitored-system blocked payload. while forcing request-local approval-required mode and leaving retries, configuration changes, and remediation authority outside the chat payload and the main Patrol page composition boundary, so once that governed - secondary area exists inside the summary shell the same payloads must not + secondary area exists inside the Current issues and history workspace the same payloads must not also drive a second page-level status strip elsewhere on the route - and the Patrol supporting-context disclosure rule, so recent changes, - learned correlations, and policy coverage stay secondary explanatory context - that opens only when degraded verification, active findings, or selected-run - investigation makes that evidence relevant instead of advertising a parallel - Patrol workflow on otherwise healthy fully verified states, and the - disclosure copy must explicitly tell operators that findings and run history - are the Patrol verification evidence while those supporting cards only add - explanation from the same governed payload family, and the Patrol-owned - helper `frontend-modern/src/features/patrol/patrolSupportingContextPresentation.ts` - must keep that transport-derived trust copy aligned across the workspace - disclosure rather than letting page shells invent local wording + and the Patrol `Details` disclosure rule, so recent changes, + learned correlations, and policy coverage stay backend and Assistant context + instead of advertising a parallel Patrol workflow on otherwise healthy fully + verified states or degraded summary health alone. First-party Patrol page + consumers must not turn those payloads into a generic Details/supporting + context panel; selected finding and run records remain the source of truth for + what the operator can inspect or do next. 9. Keep AI settings setup transport vendor-neutral: `/api/settings/ai/update` must accept provider credentials or base URLs without a baked vendor model ID, resolve the effective BYOK `model` through the canonical runtime @@ -1961,17 +2729,17 @@ the canonical monitored-system blocked payload. analysis preferences in config, but response payloads must expose only the control level and paid Patrol settings currently allowed by runtime entitlements. -10. Treat Patrol summary supporting metrics as readouts, not reinterpretations: when frontend consumers derive cards such as active findings, criticals, warnings, or fixes from the canonical payloads, those cards must stay numeric and must not synthesize new assessment labels like `Issues detected` or verification labels like `Partial verification` beneath the primary summary contract -11. Treat active Patrol runtime transport as compatible with factual activity surfaces: when the runtime is currently running, frontend consumers may surface in-progress activity context, but they must not replace the activity strip with a second assessment verdict derived from runtime state alone -12. Treat Patrol recency as a singular transport-driven fact: once header metadata, verification copy, or the findings footer already present the governed Patrol timing context, frontend summary consumers must not derive an extra timing pill from the same payloads inside the primary summary card -13. Treat Patrol findings counts as a singular supporting surface as well: when the summary shell already exposes count cards for active findings, warnings, criticals, and fixes, the primary assessment card must not repeat those same payload-derived counts as secondary badges -14. Treat Patrol schedule and recency as header-owned metadata on the main Patrol page: findings empty-state consumers should not receive or restate `next_patrol_at`, `last_patrol_at`, `last_activity_at`, or interval timing once those transport fields are already presented by the primary header and verification shell -15. Keep recovery payload filters canonical across `/api/recovery/rollups`, `/api/recovery/points`, `/api/recovery/series`, and `/api/recovery/facets`: when `internal/api/recovery_handlers.go` adds a governed recovery filter or display field such as provider-neutral `itemType`, the same normalized transport must land across all four endpoints and the contract tests must pin both outbound payload shape and accepted query aliases in the same slice -16. Keep recovery platform-query vocabulary canonical across that same `/api/recovery/*` surface: operator-facing transport must emit `platform` as the canonical query field, accepted legacy `provider` aliases must remain compatibility-only input, and `internal/api/contract_test.go` must pin that fallback behavior in the same slice as any handler change -17. Keep recovery payload platform vocabulary canonical across that same `/api/recovery/*` surface: point payloads must expose `platform`, rollup payloads must expose `platforms`, and any compatibility `provider` / `providers` aliases must remain secondary fallback fields rather than replacing the shared response model -18. Keep recovery linked-resource vocabulary canonical across that same `/api/recovery/*` surface: points and rollups must expose `itemResourceId` as the canonical linked-resource field, accepted legacy `subjectResourceId` aliases must remain compatibility-only input or secondary payload fields, and the shared proof surface must pin that normalization in the same slice as any handler change -19. Keep recovery external item-reference vocabulary canonical across that same `/api/recovery/*` surface: point and rollup payloads must expose `itemRef` as the canonical external item-reference field, accepted legacy `subjectRef` aliases must remain compatibility-only secondary payload fields, and the shared proof surface must pin that normalization in the same slice as any handler change -20. Keep first-host lookup completion explicit on the shared install-state API +11. Treat Patrol summary supporting metrics as readouts, not reinterpretations: when frontend consumers derive cards such as active findings, criticals, warnings, or fixes from the canonical payloads, those cards must stay numeric and must not synthesize new assessment labels like `Issues detected` or verification labels like `Partial verification` beneath the primary summary contract +12. Treat active Patrol runtime transport as compatible with factual activity surfaces: when the runtime is currently running, frontend consumers may surface in-progress activity context, but they must not replace the activity strip with a second assessment verdict derived from runtime state alone +13. Treat Patrol recency as a singular transport-driven fact: once header metadata, verification copy, or the findings footer already present the governed Patrol timing context, frontend summary consumers must not derive an extra timing pill from the same payloads inside the primary summary card +14. Treat Patrol findings counts as a singular supporting surface as well: when the summary shell already exposes count cards for active findings, warnings, criticals, and fixes, the primary assessment card must not repeat those same payload-derived counts as secondary badges +15. Treat Patrol schedule and recency as header-owned metadata on the main Patrol page: findings empty-state consumers should not receive or restate `next_patrol_at`, `last_patrol_at`, `last_activity_at`, or interval timing once those transport fields are already presented by the primary header and verification shell +16. Keep recovery payload filters canonical across `/api/recovery/rollups`, `/api/recovery/points`, `/api/recovery/series`, and `/api/recovery/facets`: when `internal/api/recovery_handlers.go` adds a governed recovery filter or display field such as provider-neutral `itemType`, the same normalized transport must land across all four endpoints and the contract tests must pin both outbound payload shape and accepted query aliases in the same slice +17. Keep recovery platform-query vocabulary canonical across that same `/api/recovery/*` surface: operator-facing transport must emit `platform` as the canonical query field, accepted legacy `provider` aliases must remain compatibility-only input, and `internal/api/contract_test.go` must pin that fallback behavior in the same slice as any handler change +18. Keep recovery payload platform vocabulary canonical across that same `/api/recovery/*` surface: point payloads must expose `platform`, rollup payloads must expose `platforms`, and any compatibility `provider` / `providers` aliases must remain secondary fallback fields rather than replacing the shared response model +19. Keep recovery linked-resource vocabulary canonical across that same `/api/recovery/*` surface: points and rollups must expose `itemResourceId` as the canonical linked-resource field, accepted legacy `subjectResourceId` aliases must remain compatibility-only input or secondary payload fields, and the shared proof surface must pin that normalization in the same slice as any handler change +20. Keep recovery external item-reference vocabulary canonical across that same `/api/recovery/*` surface: point and rollup payloads must expose `itemRef` as the canonical external item-reference field, accepted legacy `subjectRef` aliases must remain compatibility-only secondary payload fields, and the shared proof surface must pin that normalization in the same slice as any handler change +21. Keep first-host lookup completion explicit on the shared install-state API boundary: when `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx` receives a successful connected-agent lookup result, the canonical install @@ -1979,14 +2747,14 @@ the canonical monitored-system blocked payload. first visible platform/runtime page rather than leaving the operator on a transport-only status readout or reviving the removed `/infrastructure` route. -21. Keep the shared first-host detection contract explicit on `/api/state` as +22. Keep the shared first-host detection contract explicit on `/api/state` as used by `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`: the canonical `connectedInfrastructure` projection must stay suitable for detecting the first active reporting system during install so brand-new operators can receive the first success handoff without typing a hostname or agent ID. -22. Keep the shared first-run install-token transport explicit on +23. Keep the shared first-run install-token transport explicit on `/api/security/tokens` as used by `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`: once quick setup has produced the setup handoff credentials, the canonical @@ -1997,7 +2765,7 @@ the canonical monitored-system blocked payload. install-state surface must describe that prepared token path consistently with the live runtime behavior rather than directing the operator to create another install token manually. -23. Keep connected-infrastructure surface vocabulary canonical across the +24. Keep connected-infrastructure surface vocabulary canonical across the shared `/api/state` and reporting/install consumers: `frontend-modern/src/types/api.ts` must treat `truenas` as a first-class connected-infrastructure surface kind, and connected-infrastructure @@ -2015,13 +2783,20 @@ the canonical monitored-system blocked payload. `linkedContainerId`, so settings consumers can keep the top connections ledger scoped to top-level infrastructure without re-deriving guest status from names or local heuristics. -24. Keep AI settings payload continuity explicit on the shared `/api/settings/ai` +25. Keep AI settings payload continuity explicit on the shared `/api/settings/ai` surface: `internal/api/ai_handlers.go` and `internal/api/contract_test.go` must expose masked provider-auth state such as `ollama_username` and `ollama_password_set` without echoing raw stored secrets, and the same backend contract must keep provider test routes bound to the selected provider's configured model instead of whichever other provider currently owns the default `model` field. + Anthropic provider readiness on this shared settings payload is API-key + backed only: legacy OAuth fields may be echoed as cleanup state, but they + must not count toward `configured`, `anthropic_configured`, + `configured_providers`, model listing, provider tests, or runtime provider + construction. `/api/ai/oauth/start` and `/api/ai/oauth/exchange` fail closed + with `unsupported_anthropic_oauth`, `/api/ai/oauth/callback` must not save + tokens, and `/api/ai/oauth/disconnect` may clear stored legacy tokens. The Ollama provider payload also owns `ollama_keep_alive` as the canonical request keep-alive field: GET and update responses must expose the normalized configured value, update requests must reject malformed values, @@ -2039,7 +2814,7 @@ the canonical monitored-system blocked payload. closed before any agent command dispatch. The API progress payload must describe discovery evidence analysis rather than presenting background work as a visible Pulse Assistant chat session. -25. Keep shared AI runtime reads centralized on that same governed contract: +26. Keep shared AI runtime reads centralized on that same governed contract: `frontend-modern/src/stores/aiRuntimeState.ts` is the canonical frontend read owner for `/api/settings/ai` and `/api/ai/models`. AI-owned consumers such as `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`, @@ -2059,7 +2834,7 @@ the canonical monitored-system blocked payload. live provider catalog reads fail, so Patrol and Assistant model selectors keep saved `deepseek:` selections visible without inventing page-local catalog entries or falling back to another provider's model. -26. Keep API-backed first-target onboarding canonical on that same shared +27. Keep API-backed first-target onboarding canonical on that same shared infrastructure-settings boundary: `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`, @@ -2077,7 +2852,7 @@ the canonical monitored-system blocked payload. normalize the browser back to `/settings/infrastructure`, but first-run callers must not fall back to the retired `/settings/infrastructure/install` or `/settings/infrastructure/platforms` deep links. -27. Keep the shared agent-download fallback transport pinned to published +28. Keep the shared agent-download fallback transport pinned to published release lineage. The served install-script endpoints (/install.sh, /install.ps1) have no GitHub fallback at all (see item 8); they serve the locally bundled agent installer or fail closed. The remaining GitHub fallback @@ -2087,7 +2862,7 @@ the canonical monitored-system blocked payload. prereleases such as `v6.0.0-dev`, git-described `+git...` builds, and other unpublished prerelease identifiers must fail closed on that API boundary instead of generating fake release URLs from a local runtime version string. -28. Keep local trial-start transport retired from self-hosted v6 GA runtime +29. Keep local trial-start transport retired from self-hosted v6 GA runtime paths. `POST /api/license/trial/start` must not be registered as an ordinary in-app acquisition endpoint; browser API clients, route inventory, demo mode, and feature gates must all treat it as absent. The retired @@ -2099,7 +2874,7 @@ the canonical monitored-system blocked payload. but ordinary self-hosted responses leave eligibility false and the reason empty; active trial state is represented by `subscription_state`, `trial_expires_at`, and `trial_days_remaining` only. -29. Keep `/api/security/dev/reset-first-run` transport-backed and genuinely +30. Keep `/api/security/dev/reset-first-run` transport-backed and genuinely unauthenticated: when the dev reset route clears first-run auth it must also clear any env-backed auth state that feeds `/api/security/status`, so the status payload flips `hasAuthentication` to `false`, preserves @@ -2108,12 +2883,12 @@ the canonical monitored-system blocked payload. already-authenticated app state. That recovery transport may expose the bootstrap token file path, but it must not emit the token value into automatic runtime logs. -30. Keep shared SSO test and metadata-preview transport fail-closed: SAML +31. Keep shared SSO test and metadata-preview transport fail-closed: SAML metadata URLs and OIDC issuer URLs must reject non-HTTP or userinfo-bearing inputs before any outbound request is attempted, and OIDC discovery must append `/.well-known/openid-configuration` beneath the configured issuer base path instead of resetting to the origin root. -31. Keep config-archive import reloads fail-closed on the shared API/runtime +32. Keep config-archive import reloads fail-closed on the shared API/runtime boundary. `internal/api/config_export_import_handlers.go`, `internal/api/contract_test.go`, and adjacent config/runtime helpers must tolerate absent notification managers and other optional runtime managers @@ -2123,6 +2898,24 @@ the canonical monitored-system blocked payload. ## Current State +Manifest-backed Patrol finding lifecycle schemas are the API source of truth +for Assistant provider-tool optionality as well as MCP/API discovery. Legacy +Assistant runtime paths may project those schemas into provider-tool JSON, but +they must not re-require optional lifecycle fields such as `resolution_note` or +dismissal `note` after `/api/agent/capabilities` and the Patrol lifecycle API +declare them optional. +Legacy native Assistant utility provider aliases for `run_command`, `fetch_url`, +and `set_resource_url`, plus their provider JSON schemas, are part of the same +shared API/runtime contract through +`agentcapabilities.LegacyAssistantUtilityProviderTools`. The older native +Assistant service may keep exposing those compatibility aliases, but it must not +own separate inline provider schema maps or local tool-input argument strings +while the registry-backed execution path continues to converge. +Native Assistant registry tools that expose Patrol finding lifecycle actions +must use the same `agentcapabilities` argument constants for `finding_id`, +`resolution_note`, `reason`, and `note`, so provider tool schemas, execution +maps, `/api/agent/capabilities`, and MCP projections stay on one API vocabulary. + That same shared API boundary now also owns the default-org token scoping rule and instance-wide notification settings propagation. A token explicitly bound to one or more organizations is scoped to those organizations only and @@ -2207,7 +3000,7 @@ investigation runtime in `internal/ai/patrol_findings.go` attaches the projection to the Finding before calling the orchestrator. This is the one in-process write path that decides what the orchestrator sees about operator commitments; all consumers (in-process Patrol, -external Claude Code, future MCP) read the same enriched shape. +external Claude Code, and MCP-speaking clients) read the same enriched shape. `/api/agent/events` is the agent SSE stream — the substrate piece that closes the agent-paradigm triangle (discovery + bundled reads + @@ -2249,9 +3042,13 @@ approval store's post-create callback, the executor's post-completion callback, and the API action-execution terminal publisher) cannot stall on consumer slowness. The stream sits behind `monitoring:read` and runs through the same auth -path as the rest of the agent surface; the capabilities manifest declares the stream +path as the rest of the external-agent surface; the capabilities manifest declares the stream under `subscribe_events` so external agents discover it without out-of-band documentation. +The stable event kind strings and the stream-local transport markers are owned +by `internal/agentcapabilities/events.go`; API constants may expose typed +aliases, but API handlers, manifest descriptions, MCP adapters, and probes must +not re-declare the vocabulary independently. The approval store exposes `SetOnApprovalCreated(cb)` so the API layer can install a fire-and-forget callback that runs after every @@ -2292,31 +3089,127 @@ refusal-token prefixes (`plan_drift:`, `action_plan_expired:`, `errorMessage` verbatim so agents branch on the stable code without parsing human messages. -`/api/agent/capabilities` is the discovery document for Pulse's -agent surface. The manifest declares each agent-consumable +`/api/agent/capabilities` is the discovery document for Pulse +Intelligence's external-agent surface. The manifest declares each +agent-consumable capability with stable name (snake_case agent identifier), description, category (`context` / `provisioning` / `operator-state` / `finding` / `action`), HTTP method + path, -required auth scope, response shape name, optional JSON Schema -`inputSchema`, and the closed set of stable error codes the -response may carry. Agents fetch this once at startup to learn -what's available; the `cmd/pulse-mcp` adapter reads the manifest -to register MCP tools, and any future adapter (HTTP-API SDK, -Claude-Code custom toolkit, etc.) reads the same manifest the -same way: manifest-driven projection is the substrate's deliberate -single source of truth for "what can an agent do here?". The manifest -itself is unauthenticated and cacheable (`Cache-Control: public, +required auth scope, action mode (`read`, `mixed`, `write`), +approval policy (`scope_only`, `action_plan`), response shape name, +optional JSON Schema `inputSchema`, and the closed set of stable +error codes the response may carry. Agents fetch this once at startup +to learn what's available; the `cmd/pulse-mcp` adapter reads the +manifest's Pulse MCP `surfaceToolContracts` entry to register MCP tools, and +any future adapter (HTTP-API SDK, Claude-Code custom toolkit, etc.) reads the +same manifest surface contract the same way: +manifest-driven projection is the substrate's deliberate single source +of truth for "what can an agent do here?". The canonical manifest declaration, +manifest wire type, registry tool-name vocabulary, external tool projection +helpers, typed capability lookup error, request/response capability resolver, call route/body +projection helpers, path-parameter substitution helpers, JSON Schema +object-envelope helpers, manifest HTTP fetch, authenticated agent request +builder, capability HTTP executor, named capability HTTP executor, +named request/response capability HTTP executor, shared MCP +request decoder, line-delimited request serving, notification response policy, +stable JSON-RPC encoding, manifest-backed tool server, +surface-tool contract lookup, surface-filtered tool projection and execution, +tool-server method dispatcher, +initialize-result builder, MCP resource URI projection, context-backed +resources/list and resources/read projection, stable error-envelope formatter, +MCP tool-result text/marker interpreter, provider tool-result constructor and +context projection, +approved-action argument handling for replayed tool execution, internal +tool-argument filtering before public body projection, SSE record parser, +SSE-to-MCP notification bridge, shared tool-governance defaulting, and +disabled-control guidance live in +`internal/agentcapabilities` and are consumed by Assistant tool governance, the +API, `cmd/pulse-mcp`, and the in-repo `cmd/agent-probe` reference client; new +Pulse-native or external agent surfaces must reuse that contract instead of +defining local manifest structs, local tool-description builders, local +request/response filters, local argument-to-body shapers, local path +placeholder parsers, local `/api/agent/capabilities` fetchers, local API-token +header spelling, local named capability execution, local MCP initialize-result +builders, local MCP manifest-backed tool handlers, local MCP request decoding, +local MCP line-framing loops, local JSON-RPC encoders, local MCP notification +response checks, local MCP method dispatch, local MCP +result interpretation, local provider tool-result struct assembly, local +request/response capability body readers, local non-2xx error formatting, +local MCP resource URI builders, local resources/list or resources/read +projectors, local SSE scanners, local SSE-to-MCP notification bridges, local +tool-governance defaulting, local approved-action argument keys, local +disabled-control guidance, or local schema wrappers. Native Assistant registry +declarations must also reuse that API identity contract: every registry +`Tool.Definition.Name`, including `pulse_summarize` and Patrol runtime tools, +must come from `internal/agentcapabilities/tool_names.go` so MCP and future +external-agent adapters project the same tool identities that Assistant +executes. The manifest-backed MCP +tool server must receive the whole manifest and derive its surface-specific +tool projection, execution allowlist, and initialize surface-contract +instructions from that single value; adapters must not pass only a capability +slice and then maintain a parallel surface relationship beside it. The manifest itself is unauthenticated +and cacheable (`Cache-Control: public, max-age=300`) — declared in the router's `publicPaths` list so the global auth middleware does not gate it; the underlying capabilities keep their own auth scopes. The manifest's `unauthenticated` posture is the chicken-and-egg fix: an agent that does not yet have a token must still be able to introspect Pulse to learn how to ask for one. Adding a capability is a -deliberate "this is part of the agent surface" commitment — the +deliberate "this is part of the external-agent surface" commitment — the manifest is hand-authored rather than auto-generated so contract decisions (which capabilities are agent-stable, what the stable -error codes are, what category each belongs to) cannot drift -behind code changes. +error codes are, what category and governance posture each belongs to) cannot +drift behind code changes. Capability `scope` values are not part of that +hand-authored vocabulary: they must come from `pkg/auth` via manifest-local +aliases, matching the constants API authorization uses, rather than literal +strings that can drift from the enforced route scopes. +Every manifest capability's method, path, and advertised scope must also be +proved against the live router by projecting a concrete request through +`agentcapabilities.ProjectCapabilityCall` and requiring a wrong-scope API token +to fail with the manifest's advertised missing scope. The proof is intentionally +router-owned rather than table-owned: manifest placeholder names may stay +agent-facing, but the concrete projected route must reach the API boundary that +enforces the same scope. +API-owned Pulse Assistant dependency seams must also keep MCP as an adapter +boundary, not an internal name for the native Assistant runtime. `AIService` +provider setter types, AI handler pass-throughs, router chat dependency wiring, +AI settings control-refresh callbacks, direct approved-tool replay into chat, and the native tool adapter family must +use Assistant/Pulse Intelligence terminology, while `MCP` remains valid only +for the external adapter, shared MCP wire/result contracts, and explicitly +deprecated API compatibility aliases. The cross-repo auto-fix dependency shape must +publish `ApprovedAssistantToolExecutor` / `AssistantToolExecutor` as the native +approved-tool execution contract. Callers must use +`AIAutoFixHandlerDeps.ResolveApprovedAssistantToolExecutor`, and that resolver +returns only the native Assistant executor; MCP-named executor compatibility is +not part of this boundary. Current public API relay wiring must populate only +`AssistantToolExecutor`. +API-visible Assistant runtime readiness and lifecycle messages must name the +first-party surface as Pulse Assistant rather than the legacy generic `Pulse AI` +runtime. +API-visible shared Intelligence messages such as profile-suggestion +unavailability, AI settings persistence failures, Patrol preflight copy, and +remediation-impact logs must not narrow the shared Assistant/Patrol substrate +back to `Pulse Assistant settings` or generic `Pulse AI`; they must use Pulse +Intelligence for shared runtime availability and Provider & Models for the +shared provider/model settings surface. +Patrol finding capability scopes are API-authorization-owned, not +adapter-owned. `list_findings`, `acknowledge_finding`, `snooze_finding`, and +`dismiss_finding` must match the relay/mobile runtime route inventory, and +`resolve_finding` must match the direct `POST /api/ai/patrol/resolve` route. +Those Patrol finding review and lifecycle routes are gated by `ai:execute`, so +the manifest and MCP projection must not advertise monitoring scopes for them +while the router enforces AI execution authorization. +Capabilities that replace operator-state, mutate Patrol finding lifecycle, or +participate in the action plan/decision/execute loop must publish typed +`inputSchema` definitions for their exact top-level tool arguments, including +path parameters such as `resourceId` / `actionId`, full-replacement flags such +as `intentionallyOffline` / `neverAutoRemediate`, and stable enums such as +criticality, dismissal reasons, or approval outcomes. External adapters may +project those schemas directly, but must not replace them with adapter-local +body wrappers or prose-only argument hints. Shared manifest schemas use the +shared strict object envelope; adapter fallback schemas use the same helper with +explicitly permissive additional properties when the endpoint contract is only +partially known. Infrastructure onboarding is part of that same manifest contract, not an MCP-only shim. The provisioning category projects the @@ -2330,7 +3223,7 @@ sources returns redacted credential state only; token or password secret values may be accepted on write/test requests but must not come back through `list_nodes` or any manifest metadata. -The agent surface uses one error-envelope shape across every +The external-agent surface uses one error-envelope shape across every endpoint — `{"error": "", "message": "", "details"?: {"": ""}}` — written via `writeJSONError` (no details) or `writeJSONErrorWithDetails` @@ -2340,6 +3233,15 @@ human-readable text agents can surface to operators without losing the code; the optional `details` map carries field-level failure reasons on validation errors so agents do not parse human text to identify which field went wrong. +The envelope type, constructor, and parser live in +`internal/agentcapabilities/errors.go`; API handlers emit that shared type and +external-agent clients/probes parse it there instead of carrying local envelope +structs. +The same file owns the `AgentErrCode*` constants for every +capability-specific agent error code. The canonical manifest and the +agent-surface handlers must reference those constants rather than duplicate +string literals, so Assistant, MCP clients, probes, and API handlers share one +branching vocabulary. There are two layers of stable codes. First, **capability-specific codes** are declared per capability in the manifest's `errorCodes` @@ -2347,7 +3249,12 @@ list and represent the closed set of failure modes that capability's handler emits. Today these are: `resource_not_found` (`get_resource_context`), `operator_state_not_set` (`get_operator_state`), -`operator_state_invalid` (`set_operator_state`). +`operator_state_invalid` (`set_operator_state`); +`invalid_finding_request`, `finding_not_found`, +`finding_action_not_allowed`, and `patrol_unavailable` +(`acknowledge_finding`, `snooze_finding`, `dismiss_finding`, +`resolve_finding`); and the action-governance codes declared on +`plan_action`, `decide_action`, and `execute_action`. Adding or renaming one requires both the handler emission and the manifest declaration to move together — drift between them is a contract regression. Second, **cross-cutting codes** apply to @@ -2360,10 +3267,11 @@ request. Agents must accept these alongside any capability's declared codes; the manifest deliberately does not duplicate them on every entry. -The agent substrate is end-to-end exercised by two paired tests in +The external-agent substrate is end-to-end exercised by two paired tests in `internal/api/agent_substrate_e2e_test.go`. The first test boots the full router stack and walks discovery → triage → depth: fetch -the manifest unauthenticated, call `/api/agent/fleet-context` +the manifest unauthenticated, call `/api/agent/patrol-control/status` +authenticated, call `/api/agent/fleet-context` authenticated, drill into `/api/agent/resource-context/{id}`, and confirm the SSE stream path is gated rather than absent. The second test walks the operator-state intent loop end-to-end: @@ -2373,58 +3281,148 @@ manifest's declared error codes (`operator_state_not_set`, `operator_state_invalid`) reach the wire under the canonical `error` key and that the URL canonical id wins over any body-supplied id (preventing scope-confusion writes). Together -the two tests are the substantive proof for the agent surface — +the two tests are the substantive proof for the external-agent surface — read, write, push — as one substrate. The companion worked example lives at `cmd/agent-probe/main.go` — a small standalone Go program that walks the same discovery → triage → depth → push flow against a running Pulse instance. -The probe depends only on the standard library so it doubles as -a reference implementation: anyone building MCP servers, Claude -Code integrations, or custom agents on top of Pulse can read it -top-to-bottom to see how the substrate fits together. The probe -resolves paths from the manifest rather than hardcoding them, so -discovery moves automatically follow. +The probe avoids API handler and AI-runtime imports; it reuses only the tiny +`internal/agentcapabilities` wire/projection/HTTP contract so the in-repo +reference client cannot drift from the manifest shape, named capability HTTP +execution rule, request/response body-return helper, path projection +semantics, API-token header convention, or stable error envelope behavior; it +also consumes the shared SSE subscription +transport, record parser, and actionable-record filter so the push walkthrough +uses the same event-stream request, status, framing, and transport-event +filtering rules as the MCP bridge. External agents can define the same JSON shape from the +manifest or a generated client. Anyone building MCP servers, Claude Code +integrations, or custom agents on top of Pulse can read it top-to-bottom to see +how the substrate fits together. The probe resolves capabilities and paths from +the manifest rather than hardcoding them, so discovery moves automatically +follow. A second adapter lives at `cmd/pulse-mcp/main.go` — a minimal -MCP (Model Context Protocol) server that exposes the agent -substrate as MCP tools so Claude Desktop, Claude Code, and other -MCP-speaking clients can drive Pulse natively. The adapter is -the test for whether the substrate's contracts were really cheap -to project: each MCP tool is one entry in the hand-authored -manifest. When the manifest supplies an `inputSchema`, MCP must -use it verbatim; otherwise the adapter falls back to deriving a -minimal schema from path placeholders and method (path `{name}` -segments become required string properties; non-GET/DELETE tools -accept a `body` object). Adding a capability to the manifest -automatically extends the MCP tool surface without changes in the -adapter. `subscribe_events` is +MCP (Model Context Protocol) server that exposes Pulse Intelligence's +external-agent substrate as MCP tools so Claude Desktop, Claude Code, +OpenCode, and other MCP-speaking clients can drive Pulse +natively. The adapter is the test for whether the substrate's contracts +were really cheap to project: each MCP tool is a manifest capability allowed +by the manifest-owned Pulse MCP surface tool contract. `cmd/pulse-mcp` must use the shared +`internal/agentcapabilities` projection helpers for the complete +surface-filtered request/response tool-list projection, tools/call params +decode, surface-filtered capability lookup translation, initialize-result +construction through the target surface affordance contract, manifest fetch, neutral +capability tool HTTP execution (including path placeholder substitution, +argument-to-route/body projection, agent request construction, and capability +HTTP dispatch), MCP JSON-RPC request decoding, notification response policy, +manifest-backed MCP tool-server handlers, MCP JSON-RPC method dispatch, MCP +result wrapping, MCP resource URI projection, context-backed `resources/list` +and `resources/read` projection through `get_fleet_context` and +`get_resource_context`, shared result text projection at execution sites, +shared result text/marker interpretation, and the shared SSE-to-MCP notification +bridge. When the manifest supplies an `inputSchema`, the shared helper forwards +it verbatim; otherwise it derives a minimal fallback from path placeholders and +method (path `{name}` segments become required string properties; +non-GET/DELETE tools accept a `body` object). Adding a request/response +capability to the manifest-owned Pulse MCP surface tool contract automatically +extends the MCP tool surface without changes in the adapter. Adding a raw +manifest capability without adding it to that surface contract must not alter +MCP `tools/list` or `tools/call`. Pulse Assistant remains the first-party +in-app surface over the same +governed contracts; MCP is the external-agent adapter and must not grow a +parallel action pipeline. +MCP resource browsing follows that same rule: the adapter may advertise +resources when the target surface affordance allows resources and the manifest +carries the canonical context capabilities, but `resources/list` is only a +fleet-context projection and `resources/read` is only a resource-context +projection. Resource projection must enter through +`ManifestSurfaceResourceCapabilities`, +`ListMCPManifestSurfaceResourcesHTTP`, and +`ReadMCPManifestSurfaceResourceHTTP`; raw-capability resource helpers must not +define a second MCP resource surface. Prompt advertising and `prompts/get` +follow the same surface affordance gate before the shared workflow-prompt +catalogue is projected; initialize and `prompts/list` must use +`MCPManifestSurfacePromptProjectionSupported`, and `prompts/get` must use +`GetMCPPromptFromManifestSurface`, so disabled prompt affordances cannot be +re-enabled by raw workflow prompt presence. +The global `pulse_operations_loop` prompt is a Patrol work API manifest contract as much +as an Assistant starter: it is advertised only when the manifest includes fleet +operations-loop status, fleet context, resource context, finding list, action +planning, action decision, action execution, and finding resolution +capabilities. Its rendered instructions +must keep external agents on the same governed Patrol work flow as Pulse +Assistant by explaining Patrol evidence in context, planning first, asking for +approval or rejection only when the returned policy requires a decision, +executing when policy allows, re-reading Patrol status, context, and findings +for verification, and resolving the finding only after the outcome has been checked. +Resource URIs use the shared `pulse://resource/` shape and must +not become an adapter-local resource registry. +`subscribe_events` is intentionally excluded — SSE streaming doesn't fit the request/response tool shape; agents that need real-time push consume the SSE stream directly. The adapter speaks JSON-RPC 2.0 over stdio with line-delimited framing, and preserves the substrate's stable error envelope (`{"error": "code", "message": -"..."}`) verbatim through MCP's content-and-isError result so -agents on the MCP side branch on the same stable codes. +"..."}`) verbatim through the shared `HTTPCallResponse` to MCP +content-and-isError result helper so agents on the MCP side branch on the same +stable codes without the adapter reinterpreting response bodies or statuses. Optionally, with `--emit-notifications`, the adapter also -subscribes to `/api/agent/events` and translates each -non-transport SSE event into a JSON-RPC notification on stdout +uses the shared initialize-result builder to advertise +`experimental.pulseNotifications.kinds`, then uses the shared event bridge to +subscribe to `/api/agent/events` and write each non-transport SSE event as a +JSON-RPC notification on stdout (`notifications/finding.created`, `notifications/approval.pending`, `notifications/action.completed`); the notification's `params` is the SSE `data` payload verbatim so MCP-bound autonomous agents react to pushes without holding a separate HTTP connection. The flag is off by default because not every MCP client surfaces server-initiated notifications; transport plumbing -(`stream.connected`, `heartbeat`) is filtered. +(`stream.connected`, `heartbeat`) is filtered through the shared +agent-capabilities event vocabulary. The integration guide for `cmd/pulse-mcp` lives at -`cmd/pulse-mcp/README.md`. It carries the canonical Claude Desktop -and Claude Code config snippets, the env-var contract for the API -token, the published list of tools the manifest currently -exposes, and the documented limitations (no `subscribe_events`, -manifest fetched once at startup). External maintainers wiring +`cmd/pulse-mcp/README.md`. It carries the canonical reusable MCP runtime facts, +OpenCode's native top-level `mcp` config shape for `opencode.json` / +`opencode.jsonc`, the common `mcpServers` block for Claude-style clients, the +env-var contract for the API token, manifest-derived generated scope, +surface-filtered request/response tool, workflow prompt, and stable error-code inventories, the +context-backed MCP resource browsing behavior, and the documented limitations +(`subscribe_events` is not a callable tool; manifest fetched once at startup). +`scripts/generate-pulse-intelligence-docs.go` owns the generated public +overview and README scope/tool/prompt/error-code blocks and must use +`internal/agentcapabilities` projections rather than a second catalogue. README +tool and capability-specific error-code blocks must use the same manifest-owned +Pulse MCP surface contract as runtime `tools/list` and `tools/call`. +Manifest-owned capability titles, workflow prompt definitions, +manifest-owned `workflowPrompts` catalogue selection, MCP tool/prompt title +projection, presentation kind hints, Patrol work availability gating, and +prompt argument validation are owned by the neutral +`ManifestPulseWorkflowPrompts` / `ProjectPulseWorkflowPrompts` / +`BuildPulseWorkflowPromptFromManifest` contract; MCP keeps only protocol wire +projection and surface-specific resource URI hints. +documentation-local capability table. External maintainers wiring Pulse into their MCP-speaking client read that document, not the package source. +`docs/AGENT_SUBSTRATE.md` is the current high-level operator and integrator +summary for the same substrate. It must reflect the in-app +`Settings -> API Access -> Agent integrations` surface, both the OpenCode +native `opencode.json` / `mcp` shape and the common `mcpServers` block, and the +published `install-mcp.sh` / `install-mcp.ps1` release path; it must not +preserve old gap copy claiming there is no Pulse settings surface, no config +snippet, or no `pulse-mcp` distribution path. +`docs/releases/AGENT_PARADIGM.md` is the release-facing projection of the same +contract. It must frame `cmd/pulse-mcp` as a generic MCP adapter for +MCP-speaking clients, include OpenCode-native `mcp` setup alongside the common +`mcpServers` shape, and point full surface token guidance at the manifest-owned +`requiredScopes` list rather than hand-maintained partial scope examples. +The public `/api/agent/capabilities` manifest must also expose +`requiredScopes`, the canonical deduplicated scope list derived from its +declared capabilities through `internal/agentcapabilities.RequiredCapabilityScopes`. +Browser settings surfaces, `cmd/pulse-mcp` startup errors, MCP documentation, +probes, and future clients must consume that manifest-owned list for +full-surface token guidance instead of rebuilding a scope list from local +hardcoded assumptions. `/api/agent/resource-context/{id}` is the agent-consumable bundled context endpoint. One read returns the full situated picture of a @@ -2577,13 +3575,15 @@ that the frontend has historically called (zero current frontend consumers, verified) now back the manifest entries; the substrate's "manifest projection is cheap" promise has a documented footnote that bringing an existing endpoint into the -agent surface may require migrating its error envelope from the +external-agent surface may require migrating its error envelope from the platform-wide `APIError` shape to the agent-stable shape. That -migration was the slice 58 work for these three handlers; future -agent-surface additions on existing endpoints will pay the same -cost. The trade-off is intentional: the agent surface keeps a -single envelope contract rather than carrying a wrapper layer to -translate per capability. +migration now covers the action governance handlers and the Patrol +finding lifecycle handlers (`acknowledge_finding`, `snooze_finding`, +`dismiss_finding`, `resolve_finding`). Future external-agent additions +on existing endpoints will pay the same cost. The trade-off is +intentional: the external-agent surface keeps a single envelope +contract rather than carrying a wrapper layer to translate per +capability. Deploy-job monitored-system volume denials are retired. API routes may still accept historical payloads that mention old license-slot terminology for @@ -2628,9 +3628,145 @@ published-release truth from raw build strings. That same preview contract now includes the complete anonymous telemetry payload shape, including aggregate self-hosted adoption counters for monitored platforms, workloads, storage, and availability targets plus coarse feature -booleans. Browser callers may display or copy the exact payload, but they must -not derive hostnames, infrastructure identifiers, prompt/chat content, license -tiers, or API-token counts from it. +booleans and content-free Patrol, Assistant, and external-agent usage counters. +The frontend `TelemetryPingPreview` type in +`frontend-modern/src/api/settings.ts` must mirror every browser-visible JSON +field from `internal/telemetry.Ping`, including the resolved operations-loop, +approved-execution, first-party Assistant, external-agent, and Pulse MCP adapter +variants used by Pro value reporting, plus source-specific operations-loop +starter request counts including the Pro activation entry point. +Those Pulse Intelligence fields may describe only configured/active/governed-action +adoption state plus Assistant, Patrol detection, Patrol investigation, Patrol +resolution, aggregate external-agent use, adapter-origin MCP use, approval, +and governed-action counts inside the +rotating telemetry window. Browser callers +may display or copy the exact payload, but they must not derive hostnames, +infrastructure identifiers, prompt/chat content, command text, action output, +license tiers, token values, token counts, resource IDs, finding IDs, or +approval identities from it. +Approved action decision telemetry is part of that same anonymous API +contract: `pulse_intelligence_approved_action_decisions_30d` must count +distinct action IDs with approved-decision lifecycle evidence in the telemetry +window, or approval records whose approved decision timestamp is inside the +window for older rows that lack lifecycle events. It may count toward completed +operations-loop proof, but it must not be conflated with approved execution +attempts or approved action successes, and it must never expose action IDs, +actors, reasons, resource IDs, command text, action output, or verification +detail. +Approved execution attempt telemetry is part of that same anonymous API +contract: `pulse_intelligence_approved_action_attempts_30d` must be counted as +distinct action IDs with execution-attempt lifecycle evidence (`executing`, +`completed`, or `failed`) resolved back to an approved action audit, with final +audit-state fallback only for older rows that lack lifecycle events. It must +not be derived from generic action-plan, approval-request, token-use, Assistant +chat, or MCP call counts, and it must never expose action IDs, actors, reasons, +resource IDs, command text, or action output. +Approved action success telemetry is the stricter completion proof in that same +contract: `pulse_intelligence_approved_action_successes_30d` must count only +distinct approved action audits that reached completed state successfully, with +lifecycle-backed attribution where lifecycle evidence exists and final +audit-state fallback only for older rows. Failed or refused approved actions may +count as attempts but must not count as successes, and the success counter must +not expose action IDs, actors, reasons, resource IDs, command text, verification +detail, or action output. +Rejected action decision telemetry is the declined-governance proof in that same +contract: `pulse_intelligence_rejected_action_decisions_30d` must count distinct +action IDs with rejection-decision lifecycle evidence in the telemetry window, +with final rejected audit-state fallback only for older rows that lack lifecycle +events. It may count toward governed-action activity, but it must not count as +an approved execution attempt, approved success, or resolved operations loop +proof, and it must never expose action IDs, actors, reasons, resource IDs, +command text, or action output. +Completed operations-loop telemetry is the coarse approve/reject journey proof +in that same contract. `pulse_intelligence_complete_operations_loop_30d` and +the source-specific operations-loop booleans may be true only when the same +rotating telemetry window contains Patrol issue evidence, contextual +Assistant/MCP/external-agent collaboration, and either a rejected action +decision, an approved action decision, or approved execution-attempt evidence. +Generic Patrol runs, Patrol AI calls, action plans, and approval requests may +activate or reach the loop, but they must not complete it without issue-backed +Patrol evidence and a real decision/outcome signal. +Patrol resolution telemetry is the content-free outcome proof in that same +contract. `pulse_intelligence_patrol_resolved_findings_30d` may count only +findings whose own lifecycle reached resolved state in the telemetry window, or +findings whose investigation outcome reached `resolved` or `fix_verified` with +investigation completion evidence in that window. It must not count generic +Patrol runs, new findings, model calls, action approvals, or successful actions +without a resolved/fix-verified Patrol finding outcome, and it must never expose +finding IDs, resource IDs, finding text, remediation details, verification +detail, actors, command text, or action output. +Resolved operations loop telemetry is a derived content-free boolean: +`pulse_intelligence_resolved_operations_loop_30d` may be true only when the +same telemetry window contains Patrol resolution plus Assistant governed-context +or governed-tool collaboration or MCP/external-agent collaboration, and at +least one approved action success. In this contract, approved action success is +the shared verified-outcome predicate used by the operations-loop status route: +the approved action must have `VerificationOutcome.Status=verified` or a +canonical verification result that ran and succeeded. A bare successful +execution result is not sufficient. It is co-occurrence proof for value +reporting, not causality proof, and it must not carry any identifiers, prompts, +tool names, command payloads, action outputs, or finding text. +Patrol control completed-loop telemetry is the primary derived content-free +paid-value boolean: +`pulse_intelligence_patrol_control_completed_operations_loop_30d` may be true +only when the same telemetry window contains Patrol control starter evidence +from `pulse_patrol`, `patrol_control`, legacy `patrol_autonomy`, or legacy +`pulse_pro_activation`, Patrol issue evidence, contextual Assistant or +MCP/external-agent collaboration, and either a rejected governed decision or an +approved governed decision with verified outcome proof. It is terminal-loop +Patrol control proof, not causality proof, and it must not carry +checkout/account identity, token identity, prompt content, resource IDs, +finding IDs, action IDs, command payloads, action outputs, or verification +detail. +Patrol control resolved-loop telemetry is stricter: +`pulse_intelligence_patrol_control_resolved_operations_loop_30d` may be true +only when the same content-free window also contains an approved governed +decision and verified outcome proof. It must not treat rejected decisions as +resolved-loop proof. These telemetry booleans must use the same +`internal/telemetry.ClassifyPulseIntelligencePatrolControlProof` count-only +classifier as `GET /api/agent/patrol-control/status`, so native status, MCP +status consumers, telemetry preview, and outbound telemetry cannot diverge on +what completed, resolved, or governed-decision proof means. Legacy +`pulse_intelligence_pro_activation_completed_operations_loop_30d` and +`pulse_intelligence_pro_activation_resolved_operations_loop_30d` are +compatibility mirrors of those primary Patrol control booleans. +Paid Patrol control telemetry is a derived content-free cohort: +`pulse_intelligence_patrol_control_paid_completed_operations_loop_30d` may be +true only when the same payload also reports `paid_license=true` and +`pulse_intelligence_patrol_control_completed_operations_loop_30d=true`. +`pulse_intelligence_patrol_control_paid_resolved_operations_loop_30d` may be +true only when the same payload also reports `paid_license=true` and +`pulse_intelligence_patrol_control_resolved_operations_loop_30d=true`. Legacy +`pulse_intelligence_pro_activation_paid_completed_operations_loop_30d` and +`pulse_intelligence_pro_activation_paid_resolved_operations_loop_30d` mirror +those primary paid Patrol-control fields for cohort continuity. These fields +are paid-cohort proof for aggregate activation, retention, and free-to-paid +comparison, not causality proof; they must not carry exact plan tier, +checkout/account identity, license IDs, token identity, prompts, resources, +findings, action IDs, command payloads, action outputs, or verification detail. +External-agent recent-use telemetry is a content-free capability-activity marker +for authenticated calls into the agent/MCP contract. Every request/response +capability published through the Pulse MCP surface must map to one coarse +activity class before the route handler runs, and the marker must require the +called capability's manifest-declared scope rather than the entire manifest +scope set. Read-only external agents using `monitoring:read` must therefore +count for context/event-stream activity without being treated as action, +settings, or approval-capable callers. Generic API token last-use timestamps +are not canonical proof that the external-agent surface was used. +The `pulse-mcp` adapter may stamp the content-free +`X-Pulse-Agent-Surface: pulse_mcp` header on those same capability requests so +the telemetry snapshot can expose adapter-specific recent use while keeping the +aggregate external-agent recent-use bit valid for both direct HTTP agents and +the adapter. Route handlers must normalize unknown or absent surface markers +back to direct `agent_api` use and must never persist client identity, prompts, +request bodies, route parameters, or local resource identifiers as part of the +surface marker. +Pulse MCP `prompts/get` starter activity uses the same surface marker but a +separate route-specific payload: `POST /api/agent/workflow-prompt-activity` +records only the manifest prompt name after successful local prompt rendering. +It does not count as a capability-class request and must not infer +external-agent contextual collaboration or action execution from prompt access +alone. That same browser-transport contract now tolerates sparse preview payloads without changing the runtime truth. Patrol transport may omit `finding_ids`, and infrastructure removal previews may stage optimistic rows @@ -3512,7 +4648,7 @@ projection layer instead of by handler-local heuristics. It must not return an enforced-limit verdict or make persistence depend on a commercial volume decision. Configured Proxmox, PBS, and PMG update handlers in -internal/api/config_node_handlers.go may use that same structured +internal/api/config*node_handlers.go may use that same structured replacement-selector contract when they explain monitored-system grouping: source-owned names, host URLs, hostnames, and resource identifiers may cross the API boundary, but handler-local matcher closures must not become the @@ -3573,7 +4709,11 @@ shared access layer instead of routing directly to Patrol-local correlation state. That store now also owns the Patrol page load bundle, so the page refresh path stays aligned on one store-owned orchestration layer instead of re-encoding -the AI bundle inline. +the AI bundle inline. Frontend Patrol load orchestration must treat first-load +transport or settings failures as stale-data state rather than throwing through +the route: the page stays mounted, preserves any last-known Patrol evidence, +and exposes a retry affordance while backend/API failures remain available to +debug logging and API-level diagnostics. The AI summary page now also renders the canonical `frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx` card for policy posture, so sensitivity, routing, and redaction counts are @@ -3769,7 +4909,10 @@ customer frontend state. `frontend-modern/scripts/settings-diagnostics-boundary- now checks `internal/api/diagnostics.go` alongside the Settings diagnostics frontend boundary through the canonical frontend audit runner, so this admin-analytics payload class cannot return as a customer-visible diagnostics -contract. +contract. The same diagnostics boundary also keeps the first-party Assistant +surface out of MCP transport vocabulary: `AIChatDiagnostic` may report native +Assistant runtime availability through `assistantRuntimeConnected`, but +`mcpConnected` and `mcpToolCount` must stay out of `/api/diagnostics`. That same admin-analytics boundary now retires the local commercial metrics routes from the normal customer product API: `/api/upgrade-metrics/events`, `/api/upgrade-metrics/stats`, `/api/upgrade-metrics/health`, @@ -3796,7 +4939,7 @@ command-execution flags between display and clipboard transport. That same shared infrastructure-settings boundary now also consumes the canonical `connectedInfrastructure` projection from the backend state contract instead of reconstructing reporting rows by merging raw unified-resource facets and -removed-_ arrays in the browser. v6 clients no longer receive those removed-_ +removed-* arrays in the browser. v6 clients no longer receive those removed-\_ arrays at all for this surface; Connected infrastructure row identity, reporting-surface labels, and ignore/reconnect scope must be owned by the backend payload contract, with frontend rendering limited to @@ -4699,12 +5842,12 @@ handoff context as secondary to durable finding context, not a replacement for server-refreshed approval, resource, action, or requester authority. Frontend Patrol handoff helpers may consume current pending approval list payloads only as safe metadata for that visible briefing and any - structured `handoff_actions`: approval ID, status, risk, request/expiry - timestamps, target label, requester identity, action ID, approval policy, - plan expiry, and dry-run summary are allowed. Assessment-level visible - briefings may reuse that same safe metadata for factual action labels and safety notes, - while approval command text remains inside the governed approval/remediation - surface. +structured `handoff_actions`: approval ID, status, risk, request/expiry +timestamps, target label, requester identity, action ID, approval policy, +plan expiry, and dry-run summary are allowed. Assessment-level visible +briefings may reuse that same safe metadata for factual action labels and safety notes, +while approval command text remains inside the governed approval/remediation +surface. Patrol approval-row Assistant handoffs must use the same safe metadata boundary and set `autonomousMode:false` for the request-local chat handoff; they must not paste raw approval or action command text into a chat prompt. @@ -4739,6 +5882,11 @@ context. When a Patrol run records `error_count > 0`, the backend may include `frontend-modern/src/api/patrol.ts` must preserve those fields so the Patrol UI can explain provider/model/tool runtime failures without scraping finding copy or inferring meaning from a generic error status. +Frontend run-history presentation must derive the collapsed operator record +from the canonical run payload rather than from page-local telemetry strings: +runtime errors may read as `Patrol needs attention`, finding counts may read as +found/fixed/open/resolved work, and technical fields such as trigger reason, +tool-call count, tokens, and raw traces remain secondary forensic context. Patrol status payloads no longer carry quickstart credit state as an ordinary v6 GA API contract. `quickstart_credits_remaining`, `quickstart_credits_total`, and `using_quickstart` are retired public fields; @@ -4763,7 +5911,9 @@ source preferences from effective runtime policy. `alert_triggers_enabled` and `event_triggers_blocked`, `event_triggers_blocked_reason`, and `event_triggers_blocked_message` describe a runtime policy pause such as the local development background-automation guard. Runtime policy must not make the -transport look like the operator turned alert or anomaly triggers off. +transport look like the operator turned alert or anomaly triggers off, and the +default Patrol UI must not treat a background-only policy pause as actionable +operator guidance unless it also explains why manual Patrol is blocked. Patrol mutate endpoints that depend on the background service must also fail closed with `503 Service Unavailable` when AI service initialization is absent rather than dereferencing a nil service and crashing before a contract response @@ -5316,7 +6466,9 @@ provider-scoped test selection. `internal/api/ai_handlers.go` and `ollama_username` and `ollama_password_set`, accept provider-auth updates without echoing raw secrets back into the payload, and keep provider test routes bound to the provider's own configured model instead of whichever -other provider currently owns the default `model` selection. +other provider currently owns the default `model` selection. Legacy Anthropic +OAuth fields are cleanup-only compatibility state on that same contract and +must never revive an OAuth-backed Anthropic provider path. That same shared `/api/settings/ai` contract now also owns vendor-neutral BYOK setup. Frontend callers may submit provider credentials or base URLs without a concrete vendor model ID, and `internal/api/ai_handlers.go` must resolve and @@ -5422,12 +6574,14 @@ owners must not rely on that 402 as normal control flow for stale local state: when the current entitlement locks safe remediation, they submit `monitor` even if older persisted settings or a previous entitlement left `approval`, `assisted`, or `full` in memory. -The Patrol header presentation for that API boundary must compose the shared -`frontend-modern/src/components/shared/FilterButtonGroup.tsx` segmented -selector: the endpoint contract still owns the accepted autonomy values and -license-required response shape, while the frontend owns only the option -mapping and entitlement-derived disabled state. Local Patrol selector styling -must not become a second source of truth for the API's monitor-only clamp. +The Patrol header and configuration-dialog presentation for that API boundary +must compose the shared +`frontend-modern/src/components/shared/FilterButtonGroup.tsx` selector: the +endpoint contract still owns the accepted autonomy values and license-required +response shape, while the frontend owns only the option mapping, +entitlement-derived disabled state, and layout choice needed to keep labels +readable in the current container. Local Patrol selector styling must not become +a second source of truth for the API's monitor-only clamp. The reporting transport contract now also carries an optional narrative interpretation layer alongside the deterministic data surface. The Go-side `pkg/reporting.MetricReportRequest` gains optional `Narrator` and diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 5c4717f60..4246cfa4c 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -20,6 +20,69 @@ agreement, the Pulse Cloud control plane, provider-hosted MSP account bootstrap/licensing, hosted tenant lifecycle, and cloud-specific enforcement rules. +Pulse Pro commercial Pulse Intelligence value reporting is a cloud-paid proof +surface. It must compare full-loop, approved-execution-loop, Assistant-loop, +Assistant-resolved-loop, external-agent-loop, external-agent-resolved-loop, +Patrol-resolution, resolved-operations-loop, Patrol-guided resolved-loop, +Patrol-control completed-loop, Patrol-control resolved-loop, paid Patrol-control +completed-loop, paid Patrol-control resolved-loop, legacy Pro activation +compatibility mirrors, MCP adapter +operations-loop, MCP adapter approved-execution-loop, MCP adapter +resolved-operations-loop, and +MCP/external-agent capability-class cohorts against the generic Assistant chat +baseline with activation, retention, paid-rate, free-start conversion, and +signal-to-paid deltas. +The Patrol-guided resolved-loop cohort is the first-party completion signal: +it requires the `pulse_patrol` operations-loop starter marker plus resolved +operations-loop proof, so Patrol starter interest is not mistaken for a +completed activation loop. +The Patrol-control completed-loop cohort is a first-class telemetry signal for +historical conversion and retention analysis. It requires the Patrol control +starter or its legacy Pro activation entry-point alias, Patrol issue evidence, +contextual Assistant or external-agent collaboration, and either a rejected +governed decision or an approved governed decision with verified outcome proof, +so a deliberate operator rejection can complete the decision loop without being +misreported as an approved-and-resolved outcome. Local self-hosted Pro +operations-status presentation must apply the same distinction through the +shared operations-loop `patrolControlValueState`, falling back to +`patrolAutonomyValueState` and then `proActivationValueProofState` only for +compatibility: `governed_decision_recorded` remains partial decision status +with a continue path, legacy `verified_needs_mcp` is treated as verified +first-party Patrol value with MCP readiness left as optional external-agent +context, and `verified` may be presented as a verified Patrol operations +outcome or suppress a new starter marker. Paid-plan status rows must describe +missing or partial Pro action entitlements as Patrol investigation/remediation +capability, not as generic `Pro operations capability`, so the commercial +surface tells users which governed Patrol behavior is affected. +The Patrol-control resolved-loop cohort is a first-class telemetry signal for +paid Patrol control value, not a report-time intersection of generic starter +interest and generic resolved-loop proof. Pulse emits it only when a Patrol +control or legacy Pro activation starter, Patrol issue evidence, contextual +Assistant or external-agent collaboration, an approved governed decision, and +verified outcome proof are present, and the license-server value report must +consume that explicit primary flag. +The paid Patrol-control completed-loop and resolved-loop cohorts are also +first-class telemetry signals. Pulse emits them only when the current coarse +`paid_license` posture coexists with the corresponding Patrol-control +completed-loop or resolved-loop proof in the same telemetry payload. Legacy Pro +activation completed/resolved and paid cohort fields may mirror those primary +Patrol-control values for continuity. The value report must use those explicit +booleans for paid-rate, retention, and free-to-paid comparisons, and must not +infer paid Patrol-control value from exact tiers, account links, checkout +sessions, license IDs, or a report-time join against customer identity. +Approved-execution-loop +metrics mean Patrol issue evidence plus Assistant governed-context, direct +external-agent, or MCP collaboration plus at least one approved governed-action +attempt; complete-loop metrics mean Patrol issue evidence plus contextual collaboration plus an +approved or rejected governed-action decision signal. Those metrics must remain +separate from broader governed-action activity so action plans and approval +requests are not mistaken for completed operations-loop proof. Capability-class signal-to-paid +metrics must use the first time that specific capability was observed while +the install was still free, then count conversion only when paid activation +happened after that capability signal; paid installs that first use a +capability after purchase must not be counted as capability-driven conversion +proof. + Provider-hosted MSP bootstrap output is part of the cloud-paid operator contract. It must carry the resolved MSP plan version, plan source, workspace limit, and validated provider MSP license id/email so installability proof can @@ -88,7 +151,7 @@ Stripe-free and avoids a cloud-control-plane report data path across clients. 27. `internal/cloudcp/entitlements/service.go` 28. `internal/cloudcp/portal/handlers.go` 29. `internal/cloudcp/portal/page.go` -29a. `internal/cloudcp/portal/setup_facts.go` + 29a. `internal/cloudcp/portal/setup_facts.go` 30. `internal/cloudcp/public_cloud_signup_handlers.go` 31. `internal/cloudcp/registry/models.go` 32. `internal/cloudcp/registry/registry.go` @@ -97,7 +160,7 @@ Stripe-free and avoids a cloud-control-plane report data path across clients. 35. `internal/hosted/provisioner.go` 36. `frontend-modern/src/App.tsx` 37. `frontend-modern/src/AppLayout.tsx` -37a. `frontend-modern/src/components/CommercialMigrationBanner.tsx` + 37a. `frontend-modern/src/components/CommercialMigrationBanner.tsx` 38. `frontend-modern/src/useAppRuntimeState.ts` 39. `frontend-modern/src/components/Settings/BillingAdminPanel.tsx` 40. `frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx` @@ -129,20 +192,20 @@ Stripe-free and avoids a cloud-control-plane report data path across clients. 66. `pkg/licensing/testing_helpers.go` 67. `pkg/licensing/self_hosted_feature_catalog.go` 68. `frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts` -80. `internal/cloudcp/server.go`, `internal/cloudcp/authz.go`, `internal/cloudcp/commercial_identity.go`, `internal/cloudcp/security.go` -81. `internal/cloudcp/health_monitor.go`, `internal/cloudcp/health_stuck_provisioning.go`, `internal/cloudcp/tenant_state_metrics.go`, `internal/cloudcp/ratelimit.go` -81a. `internal/cloudcp/proxytrust/client_ip.go` -82. `internal/cloudcp/hosted_entitlement_handlers.go`, `internal/cloudcp/url_helpers.go` -83. `internal/cloudcp/admin/handlers.go`, `internal/cloudcp/admin/status.go`, `internal/cloudcp/auditlog/auditlog.go` -84. `internal/cloudcp/cpmetrics/metrics.go`, `internal/cloudcp/cpsec/nonce.go`, `internal/cloudcp/static_assets.go`, `internal/cloudcp/favicon.svg` -85. `internal/cloudcp/email/sender.go`, `internal/cloudcp/email/templates.go` -86. `internal/cloudcp/handoff/handler.go`, `internal/cloudcp/handoff/handoff.go` -87. `internal/cloudcp/stripe/grace_enforcer.go`, `internal/cloudcp/stripe/helpers.go`, `internal/cloudcp/stripe/reconciler.go`, `internal/cloudcp/stripe/webhook.go` -88. `internal/hosted/hosted_metrics.go`, `internal/hosted/reaper.go` -89. `internal/cloudcp/public_msp_signup_handlers.go` -90. `internal/cloudcp/provider_msp_bootstrap.go` -91. `internal/cloudcp/provider_msp_backup.go` -92. `internal/cloudcp/provider_msp_recovery.go` +69. `internal/cloudcp/server.go`, `internal/cloudcp/authz.go`, `internal/cloudcp/commercial_identity.go`, `internal/cloudcp/security.go` +70. `internal/cloudcp/health_monitor.go`, `internal/cloudcp/health_stuck_provisioning.go`, `internal/cloudcp/tenant_state_metrics.go`, `internal/cloudcp/ratelimit.go` + 81a. `internal/cloudcp/proxytrust/client_ip.go` +71. `internal/cloudcp/hosted_entitlement_handlers.go`, `internal/cloudcp/url_helpers.go` +72. `internal/cloudcp/admin/handlers.go`, `internal/cloudcp/admin/status.go`, `internal/cloudcp/auditlog/auditlog.go` +73. `internal/cloudcp/cpmetrics/metrics.go`, `internal/cloudcp/cpsec/nonce.go`, `internal/cloudcp/static_assets.go`, `internal/cloudcp/favicon.svg` +74. `internal/cloudcp/email/sender.go`, `internal/cloudcp/email/templates.go` +75. `internal/cloudcp/handoff/handler.go`, `internal/cloudcp/handoff/handoff.go` +76. `internal/cloudcp/stripe/grace_enforcer.go`, `internal/cloudcp/stripe/helpers.go`, `internal/cloudcp/stripe/reconciler.go`, `internal/cloudcp/stripe/webhook.go` +77. `internal/hosted/hosted_metrics.go`, `internal/hosted/reaper.go` +78. `internal/cloudcp/public_msp_signup_handlers.go` +79. `internal/cloudcp/provider_msp_bootstrap.go` +80. `internal/cloudcp/provider_msp_backup.go` +81. `internal/cloudcp/provider_msp_recovery.go` ## Shared Boundaries @@ -198,94 +261,94 @@ Stripe-free and avoids a cloud-control-plane report data path across clients. syntactically valid `/api/public/signup` requests resolve to one uniform `202 Accepted` Pulse Account response whether provisioning/email side effects ran or were suppressed by owner-email throttling. -7. `internal/cloudcp/auth/magiclink.go` shared with `security-privacy`: control-plane magic-link HMAC handling is both a Pulse Cloud account-access boundary and a security/privacy token-secrecy boundary. -8. `internal/cloudcp/auth/magiclink_store.go` shared with `security-privacy`: control-plane magic-link persistence is both a Pulse Cloud account-access boundary and a security/privacy storage-hardening boundary. -9. `internal/cloudcp/docker/labels.go` shared with `deployment-installability`: hosted tenant Docker labels are both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary. -10. `internal/cloudcp/docker/manager.go` shared with `deployment-installability`: hosted tenant container management is both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary. - Hosted tenant container creation must also bound Docker `json-file` logs - through the control-plane Docker manager so tenant runtime logging cannot - fill the live Pulse Cloud host independently of tenant data quotas. - Hosted tenant container creation must also stamp tenant identity into the - runtime environment: the Docker manager resolves the workspace display name - from the tenant registry at container-create time and injects - `PULSE_TENANT_NAME` alongside `PULSE_TENANT_ID`, so alert webhook payloads - from hosted client runtimes carry a human-readable workspace label instead - of falling back to the tenant ID. Display-name changes after creation apply - on the next tenant runtime rollout, which recreates the container with - freshly resolved environment. - Provider-hosted MSP and Pulse-hosted tenant creation must also prepare or - fail closed on the configured tenant runtime image before mutating the - workspace into a state that expects a live runtime container; readiness - checks must surface missing Docker daemon, network, image, and storage - prerequisites as operator-facing failures. - Hosted checkout and MSP workspace provisioning must also pass the - control-plane storage admission guard before tenant/account mutation: root - filesystem, tenant data, Docker runtime store, and Docker build-cache - thresholds are part of the Cloud paid readiness contract rather than an - operator-only cleanup script. - The same cloud audit contract must fail on stale proof/canary account rows - and paid hosted entitlements whose tenant rows are missing, because either - residue can recreate or mask hosted runtime state after a cleanup. - Disposable proof-account cleanup must use the control-plane registry as the - source of truth rather than ad hoc SQL: account deletion must refuse accounts - that still own tenant rows, remove account-owned membership, invitation, and - Stripe account metadata in one registry transaction, and leave user identity - rows intact unless a separately governed identity-retention rule says - otherwise. - The live production host must run that cloud audit through the private - Pulse Pro operations bundle on a recurring systemd timer, write durable - status/log output, and emit Prometheus textfile metrics so a clean GA - baseline is continuously monitored rather than manually rediscovered. - Provider-hosted MSP runtime isolation must keep client tenant runtimes off - the shared provider ingress/support network. The Docker manager must derive - a per-client tenant bridge, label it with tenant identity, pin Traefik - routing to that tenant bridge, attach only provider support containers - selected by provider MSP labels, start the tenant runtime only after those - support attachments succeed, prefer the tenant bridge for health checks, and - remove tenant bridges when the owning runtime is removed. - Tenant runtime containers must also start with the provider-hosted escape - hardening defaults owned by the Docker manager: no-new-privileges, dropped - Linux capabilities unless explicitly reintroduced by a governed need, - Docker's default seccomp profile still active, a read-only root filesystem, - and bounded writable tmpfs mounts only for runtime scratch paths. These - defaults are part of the client isolation contract, not optional compose - decoration. - Tenant runtime containers must also start as the rootless Pulse runtime user - instead of entering the image's root-owned chown and `su-exec` branch. The - Docker manager owns host-side preparation of the tenant data directory, - including recursive ownership alignment to the configured tenant UID/GID and - strict permissions on tenant key files, so `CapDrop: ["ALL"]` and - read-only root filesystems remain compatible with first workspace startup. - Client-bound proof must cover the boundary itself: workspace limits must not - be raceable past the licensed cap, handoff tokens must not be replayed or - retargeted across workspaces, org-bound agent install/report tokens must not - write into another client runtime, and rotated-out install tokens must fail - immediately. - Provider-default report branding is part of the same tenant environment - contract: `internal/cloudcp/docker/manager.go` may inject - `PULSE_REPORT_PROVIDER_BRAND_DISPLAY_NAME`, logo path/data, and logo format - into each tenant container, but it must not collect report data or render - PDFs in the control plane. Tenant-local reporting and tenant-local licensing - decide whether that configured brand appears. - `pulse_hosted_msp` is the Pulse-operated form of the same Stripe-free MSP - control-plane family, not the public Pulse-hosted SaaS checkout path. It - must share the license-backed MSP plan source, workspace limit policy, - disabled public signup and Stripe billing routes, isolated tenant runtime - networks, tenant-local report branding, and provider MSP operator commands - with `provider_hosted_msp` while preserving its own control-plane mode value - for status, backup manifests, and operational audit. -11. `internal/cloudcp/provider_msp_backup.go` shared with `deployment-installability`: provider-hosted MSP backup is both a cloud-paid license/account/runtime continuity boundary and a deployment-installability recovery artifact boundary. +9. `internal/cloudcp/auth/magiclink.go` shared with `security-privacy`: control-plane magic-link HMAC handling is both a Pulse Cloud account-access boundary and a security/privacy token-secrecy boundary. +10. `internal/cloudcp/auth/magiclink_store.go` shared with `security-privacy`: control-plane magic-link persistence is both a Pulse Cloud account-access boundary and a security/privacy storage-hardening boundary. +11. `internal/cloudcp/docker/labels.go` shared with `deployment-installability`: hosted tenant Docker labels are both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary. +12. `internal/cloudcp/docker/manager.go` shared with `deployment-installability`: hosted tenant container management is both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary. + Hosted tenant container creation must also bound Docker `json-file` logs + through the control-plane Docker manager so tenant runtime logging cannot + fill the live Pulse Cloud host independently of tenant data quotas. + Hosted tenant container creation must also stamp tenant identity into the + runtime environment: the Docker manager resolves the workspace display name + from the tenant registry at container-create time and injects + `PULSE_TENANT_NAME` alongside `PULSE_TENANT_ID`, so alert webhook payloads + from hosted client runtimes carry a human-readable workspace label instead + of falling back to the tenant ID. Display-name changes after creation apply + on the next tenant runtime rollout, which recreates the container with + freshly resolved environment. + Provider-hosted MSP and Pulse-hosted tenant creation must also prepare or + fail closed on the configured tenant runtime image before mutating the + workspace into a state that expects a live runtime container; readiness + checks must surface missing Docker daemon, network, image, and storage + prerequisites as operator-facing failures. + Hosted checkout and MSP workspace provisioning must also pass the + control-plane storage admission guard before tenant/account mutation: root + filesystem, tenant data, Docker runtime store, and Docker build-cache + thresholds are part of the Cloud paid readiness contract rather than an + operator-only cleanup script. + The same cloud audit contract must fail on stale proof/canary account rows + and paid hosted entitlements whose tenant rows are missing, because either + residue can recreate or mask hosted runtime state after a cleanup. + Disposable proof-account cleanup must use the control-plane registry as the + source of truth rather than ad hoc SQL: account deletion must refuse accounts + that still own tenant rows, remove account-owned membership, invitation, and + Stripe account metadata in one registry transaction, and leave user identity + rows intact unless a separately governed identity-retention rule says + otherwise. + The live production host must run that cloud audit through the private + Pulse Pro operations bundle on a recurring systemd timer, write durable + status/log output, and emit Prometheus textfile metrics so a clean GA + baseline is continuously monitored rather than manually rediscovered. + Provider-hosted MSP runtime isolation must keep client tenant runtimes off + the shared provider ingress/support network. The Docker manager must derive + a per-client tenant bridge, label it with tenant identity, pin Traefik + routing to that tenant bridge, attach only provider support containers + selected by provider MSP labels, start the tenant runtime only after those + support attachments succeed, prefer the tenant bridge for health checks, and + remove tenant bridges when the owning runtime is removed. + Tenant runtime containers must also start with the provider-hosted escape + hardening defaults owned by the Docker manager: no-new-privileges, dropped + Linux capabilities unless explicitly reintroduced by a governed need, + Docker's default seccomp profile still active, a read-only root filesystem, + and bounded writable tmpfs mounts only for runtime scratch paths. These + defaults are part of the client isolation contract, not optional compose + decoration. + Tenant runtime containers must also start as the rootless Pulse runtime user + instead of entering the image's root-owned chown and `su-exec` branch. The + Docker manager owns host-side preparation of the tenant data directory, + including recursive ownership alignment to the configured tenant UID/GID and + strict permissions on tenant key files, so `CapDrop: ["ALL"]` and + read-only root filesystems remain compatible with first workspace startup. + Client-bound proof must cover the boundary itself: workspace limits must not + be raceable past the licensed cap, handoff tokens must not be replayed or + retargeted across workspaces, org-bound agent install/report tokens must not + write into another client runtime, and rotated-out install tokens must fail + immediately. + Provider-default report branding is part of the same tenant environment + contract: `internal/cloudcp/docker/manager.go` may inject + `PULSE_REPORT_PROVIDER_BRAND_DISPLAY_NAME`, logo path/data, and logo format + into each tenant container, but it must not collect report data or render + PDFs in the control plane. Tenant-local reporting and tenant-local licensing + decide whether that configured brand appears. + `pulse_hosted_msp` is the Pulse-operated form of the same Stripe-free MSP + control-plane family, not the public Pulse-hosted SaaS checkout path. It + must share the license-backed MSP plan source, workspace limit policy, + disabled public signup and Stripe billing routes, isolated tenant runtime + networks, tenant-local report branding, and provider MSP operator commands + with `provider_hosted_msp` while preserving its own control-plane mode value + for status, backup manifests, and operational audit. +13. `internal/cloudcp/provider_msp_backup.go` shared with `deployment-installability`: provider-hosted MSP backup is both a cloud-paid license/account/runtime continuity boundary and a deployment-installability recovery artifact boundary. License-backed provider MSP backups must include the signed MSP license file as a recovery artifact while exposing only license metadata in command output, restore must recover that license as an explicit operator artifact, and the archive must stay Stripe-free so provider-hosted MSP recovery does not inherit Pulse-hosted SaaS billing assumptions. -12. `internal/cloudcp/provider_msp_recovery.go` shared with `deployment-installability`: provider-hosted MSP failed-workspace recovery is both a cloud-paid license/account/runtime continuity boundary and a deployment-installability recovery artifact boundary. +14. `internal/cloudcp/provider_msp_recovery.go` shared with `deployment-installability`: provider-hosted MSP failed-workspace recovery is both a cloud-paid license/account/runtime continuity boundary and a deployment-installability recovery artifact boundary. Provider-hosted MSP recovery must require the signed provider MSP license source by default, preserve the client workspace boundary, refuse to start from empty tenant data, and mark a workspace active only after the canonical tenant-runtime rollout path has produced a healthy runtime. -13. `internal/cloudcp/tenant_runtime_rollout.go` shared with `deployment-installability`: hosted tenant runtime rollout is both a Pulse Cloud runtime contract boundary and a deployment-installability release-rollout boundary. +15. `internal/cloudcp/tenant_runtime_rollout.go` shared with `deployment-installability`: hosted tenant runtime rollout is both a Pulse Cloud runtime contract boundary and a deployment-installability release-rollout boundary. Hosted tenant runtime reconciliation must treat a registered tenant with preserved tenant data but no live Docker runtime as a recoverable managed state, not as a terminal skip. The control-plane-owned reconcile path must @@ -303,7 +366,7 @@ Stripe-free and avoids a cloud-control-plane report data path across clients. must print the target-image rollout plan before mutation, `--all` must select active tenants only, and apply must still use the canonical per-tenant snapshot, health-check, and rollback path. `tenant-runtime - reconcile --all` remains contract/routing drift repair for each tenant's +reconcile --all` remains contract/routing drift repair for each tenant's current image line, not an image-line upgrade path. The real `pulse-pro` license-server legacy checkout issuance, recurring @@ -323,6 +386,112 @@ site deploys; active paid-feature proof must stay on checkout creation, portal-handoff state, pricing/catalog truth, download delivery, and license or relay entitlement behavior rather than requiring the removed funnel monitoring files. +Pulse Intelligence paid-value evidence is a separate private admin aggregate, +not a revival of those retired funnel stores. The `pulse-pro` license server +may expose `/v1/admin/pulse-intelligence/value` and the matching admin-console +strip only as coarse install cohorts over anonymous telemetry pings: paid +install rate, free-to-paid conversion, returning-install retention, and whether +the full Patrol -> contextual collaboration -> governed action loop, the +stricter Patrol -> contextual collaboration -> approved execution loop, and +the approved-action-success and adapter-specific Pulse MCP loop variants are +active, including the stricter resolved operations loop split by first-party +Assistant collaboration, external-agent collaboration, and Pulse MCP adapter +use. +The same aggregate may expose an `operations_funnel` object that keeps the +loop proof readable as a staged anonymous cohort sequence: loop configured, +Patrol activity, Assistant or external-agent collaboration, governed action, +approved execution, approved action success, Patrol resolution, resolved +operations loop, Assistant resolved loop, external-agent resolved loop, active +30-day use, retained installs, Patrol-control completed-loop and resolved-loop +proof, paid Patrol-control cohorts, and full-loop free-to-paid conversion compared with generic +Assistant-only chat. That funnel is a projection over the same coarse telemetry +counters and value cohorts; it must not introduce per-install drilldowns or raw +commercial account links. Source-specific guided starter cohorts may split the +same anonymous operations-loop starter count into Assistant, Patrol, primary +Patrol-control, legacy Pro activation entry-point, and Pulse MCP sources so +conversion reporting can tell which entry point led to loop entry, but those cohorts +remain starter evidence only and must not be counted as completed +operations-loop, approved execution, or resolved-loop proof. +Patrol-control completed-loop and resolved-loop funnel stages must come from +the explicit content-free Patrol-control booleans; resolved proof may satisfy +completed proof, but a rejected terminal completed loop must never be counted as +resolved proof. Legacy Pro activation completed/resolved booleans may be +retained only as mirrors for historical cohort continuity. +The self-hosted Pro plan surface may consume the authenticated, content-free +`GET /api/agent/operations-loop/status` projection only to choose the primary +Patrol handoff in the current-plan summary: set Patrol control when no work is +active, continue unfinished Patrol work, or review a pending governed decision. +The plan capability details disclosure must stay entitlement/runtime +diagnostics only. It must not add a `Patrol work`, started, +decision-recorded, verified, external-agent readiness, or compatibility +operations-loop status row from Patrol starter/completed/resolved counts, +legacy `proActivation*` aliases, legacy `patrolAutonomy*` mirrors, shared +status `nextAction`, or shared status `progressLabel`; those fields are +Patrol-page context and telemetry, not paid-plan detail copy. The plan surface +must not expose prompts, accounts, resource names, finding IDs, action IDs, +commands, token metadata, or any per-install drilldown, and lack of that +projection must not block entitlement or plan rendering. The visible Pro plan +card must keep the paid user's first-party task clear: choose Patrol mode and +let Patrol handle infrastructure work. The first-party commercial copy owns +the product framing: stale activation-loop, operations-loop, or MCP setup +progress text from the projection must not become the visible paid CTA when +the correct next step is Patrol control or Patrol work. +Commercial, plan, and activation-success copy must use `Choose Patrol mode` +for the paid operator boundary and frame it as setting how autonomous Patrol +should be. Self-hosted Community copy should state the features available on +that install, such as watch-only Patrol, instead of explaining unavailable +fix classes. Pro plan cards may describe hands-on Patrol modes and verified +fixes, but they must not revive activation-loop, MCP readiness, or internal +proof vocabulary as the paid user's default setup story; route-backed anchors, +telemetry, and compatibility wire fields may retain stable names such as +`patrol_control` and `patrolControl*`. +Routine self-hosted Plans and capability-status copy must describe what is +available on the instance and route recovery as `refresh the plan` or `open +recovery`; it must not describe normal setup as activation or expose raw +entitlement-payload wording to the operator. +That external-agent readiness boolean means the manifest-owned Pulse MCP +operations-loop contract is available and one non-expired API token covers the +full published operations-loop scope set; a partial MCP token is not paid value +proof, missing MCP readiness must not downgrade verified Patrol value, and it +must not become an activation/setup step on the default plan surface. +The completed-loop count consumed here is the API-owned terminal-loop proof: it +requires Patrol control starter evidence, Patrol issue evidence, contextual +Assistant or external-agent collaboration, and either a rejected governed +decision or an approved governed decision with verified outcome proof. +The resolved-loop count consumed here is already the API-owned full-loop proof: +it requires Patrol control starter evidence, Patrol issue evidence, contextual +Assistant or external-agent collaboration, an approved governed decision, and a +verified outcome in the same evidence window. +The steady-state current-plan action may use that same first-party Patrol +control projection to label the Patrol handoff as start, continue, or review, +but it must still route to the Patrol-owned Patrol control anchor rather than +creating a separate commercial activation state machine. Legacy Pro activation +evidence alone must leave the action as a fresh Patrol control handoff. +Start and continue paid Patrol control handoffs from +`frontend-modern/src/utils/licensePresentation.ts` must compose the +route-backed Patrol control URL from +`frontend-modern/src/routing/resourceLinks.ts`; the settings panel must not +write the starter marker from a local click handler before navigation, and its +presentation model must not carry a `recordActivity` switch for that handoff. +Patrol owns consuming that route flag and writing the content-free +`patrol_control` marker; `patrol_autonomy` and `pulse_pro_activation` remain +accepted only as legacy route/activity aliases. Once the action has become verified review, +`licensePresentation.ts` must route to the plain Patrol control anchor and +keep marker recording disabled. +When that action has become a completed-loop review, it must not write another +Patrol control starter marker; starter telemetry remains for start/continue +entry into the guided journey rather than for post-completion review clicks. +The value report may also expose a `value_driver_evidence` summary derived only +from those same anonymous cohort deltas. That summary may state whether the +available evidence is insufficient, positive on paid conversion, positive on +activation or retention, or has no positive delta against the generic +Assistant-only baseline; it must not claim causality, store per-install +drilldowns, or introduce a second analytics data source outside telemetry +pings. +That report must not expose install IDs, customer emails, prompts, finding IDs, +resource IDs, or action payloads, and generic Assistant-only chat must remain a +separate cohort so it cannot be counted as proof that the proactive operations +loop is driving paid value. That same public-pricing boundary also owns the machine-readable v6 pricing validator and launch checklist in `pulse-pro:scripts/validate_public_pricing_model.py` and `pulse-pro:V6_LAUNCH_CHECKLIST.md`. Those operator surfaces must describe @@ -602,7 +771,27 @@ or other self-hosted uncapped continuity plans. actions are cloud-paid behaviors, but their button chrome must compose the frontend-primitives `Button` family instead of carrying Pro-license-local primary, secondary, or warning action class strings. -18. Add or change paid relay settings and pairing presentation through `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx`, `frontend-modern/src/components/Settings/RelayPairingSection.tsx`, and `frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts`. The retired Dashboard shell must not be restored to carry a Relay onboarding card or equivalent blanket upsell — relay discovery stays inside its owning settings surface. + The self-hosted Pro plan and activation-success surfaces must keep Patrol + actions tied to current operator work: set Patrol control when no work is + active, open Patrol for current work, or review a pending governed + decision. Self-hosted Pro plan status and readiness payloads are support + diagnostics: the default Plan surface must keep the current plan and Patrol + mode action primary, while capability details render behind a disclosure + for troubleshooting unavailable controls or missing capabilities. That + disclosure must not duplicate Patrol-page work progress, decisions, + verified outcomes, or external-agent setup readiness. + Completed verified Patrol outcomes are history/telemetry and must not + appear as a default `Patrol history` CTA or a `Patrol work` verified + status row in the Plan surface. Frontend presentation models must call + this a Patrol control action/status boundary; `operations-loop` wording is + only acceptable at backend API or telemetry compatibility edges. + The Plan surface may fetch Patrol operator status from the compatibility + `/api/agent/operations-loop/status` endpoint only after the current + self-hosted plan resolves to a Pro-family tier through + `getSelfHostedPlanDefinitionForBillingTier`. Community and Relay plans + must render their included capabilities without loading paid Patrol + operator-status plumbing. +17. Add or change paid relay settings and pairing presentation through `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx`, `frontend-modern/src/components/Settings/RelayPairingSection.tsx`, and `frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts`. The retired Dashboard shell must not be restored to carry a Relay onboarding card or equivalent blanket upsell — relay discovery stays inside its owning settings surface. Public demo and other read-only presentation policy states must suppress relay setup and upsell onboarding instead of inviting pairing or commercial action from a governed non-manageable surface. @@ -613,8 +802,8 @@ or other self-hosted uncapped continuity plans. Relay settings and public commercial docs must also keep Remote Access positioned as a Relay-and-higher capability, not a Pro-only feature, so the Relay tier remains a tangible standalone paid product. -20. Add contract tests where runtime and pricing need to stay aligned -21. Add or change hosted browser org-context bootstrap through `frontend-modern/src/App.tsx`, `frontend-modern/src/AppLayout.tsx`, `frontend-modern/src/useAppRuntimeState.ts`, and `frontend-modern/src/utils/apiClient.ts` +18. Add contract tests where runtime and pricing need to stay aligned +19. Add or change hosted browser org-context bootstrap through `frontend-modern/src/App.tsx`, `frontend-modern/src/AppLayout.tsx`, `frontend-modern/src/useAppRuntimeState.ts`, and `frontend-modern/src/utils/apiClient.ts` That same hosted bootstrap boundary also owns the runtime-capability JSON shape that the app shell consumes before it decides whether organization chrome and multi-tenant routes exist. `pkg/licensing/entitlement_payload.go` @@ -637,7 +826,7 @@ or other self-hosted uncapped continuity plans. compatibility routes must not revive hosted signup, public Cloud trial, pricing, or commercial acquisition surfaces inside the local product runtime. -22. Keep the hosted account portal shell task-first and compact. Section +20. Keep the hosted account portal shell task-first and compact. Section headers, billing action rows, and the maintained portal bundle under `internal/cloudcp/portal/` may surface the facts an operator needs, but the shell should not spend vertical space on duplicate context-chip strips when @@ -646,7 +835,7 @@ or other self-hosted uncapped continuity plans. the section, but that summary treatment must stay inside the same task surface instead of reviving a separate overview panel, summary strip, or metric deck ahead of the real workspace list. -23. Keep monitored-system volume out of the commercial product model. +21. Keep monitored-system volume out of the commercial product model. Recognized self-hosted and hosted v6 tiers must treat legacy monitored-system limit, continuity, or `max_monitored_systems` metadata as scrub-only compatibility input rather than as support/audit policy or a @@ -662,12 +851,13 @@ or other self-hosted uncapped continuity plans. narrative across the in-app billing shell, Pulse Account handoff, public pricing contract, and owned upgrade reasons. Customer-facing self-hosted copy must keep the ladder explicit as `Community = monitor`, `Relay = reach - Pulse from anywhere plus pair Pulse Mobile for handoff`, and `Pro = investigate root cause, use safe - remediation workflows, and retain 90-day history`. Team/admin extras such as +Pulse from anywhere plus pair Pulse Mobile for handoff`, and `Pro = +hands-on Patrol modes, issue investigation, verified fixes, and 90-day history`. + Team/admin extras such as RBAC, SSO, audit logging, reporting, and agent profiles may remain present, but they are secondary included value and must not displace that operator outcome framing on owned commercial surfaces. -24. Keep public-demo route bootstrap owned on the adjacent +22. Keep public-demo route bootstrap owned on the adjacent commercial/runtime boundary. `frontend-modern/src/useAppRuntimeState.ts` may prewarm shared infrastructure summary caches for route-owned surfaces, but public-demo arrival must not front-run a broader infrastructure-summary @@ -685,25 +875,36 @@ or other self-hosted uncapped continuity plans. browser entrypoint must remain only a thin authenticated redirect rather than a second public handoff or a route that invites separate commercial posture logic. -25. Keep self-hosted commercial plan copy aligned with the v6 operator-value +23. Keep self-hosted commercial plan copy aligned with the v6 operator-value model instead of mirroring raw entitlement keys. `frontend-modern/src/ - utils/selfHostedPlans.ts` may still map onto internal capabilities such as +utils/selfHostedPlans.ts` may still map onto internal capabilities such as `ai_patrol`, `ai_alerts`, `ai_autofix`, and `kubernetes_ai`, but the customer-facing plan cards, top-summary highlights, and comparison rows must lead with the canonical plan story: `Community = monitor`, `Relay = - reach Pulse from anywhere plus pair Pulse Mobile for handoff`, and `Pro = root-cause analysis, safe - remediation workflows, and longer history`. Team/admin capabilities such as +reach Pulse from anywhere plus pair Pulse Mobile for handoff`, and `Pro = +hands-on Patrol modes, issue investigation, verified fixes, and longer history`. + Team/admin capabilities such as RBAC, audit logging, reporting, and agent profiles may appear as - secondary included extras, while platform-specific compatibility keys such + secondary included extras. Their customer-facing plan summary label is + `admin controls`, not `admin tools`, because Pro is sold as Patrol handling + infrastructure within a chosen mode rather than as a bag of operator + utilities. Platform-specific compatibility keys such as `kubernetes_ai` must not be elevated into a marquee marketed Pro line - item on the self-hosted Plans surface. Legacy packaging nouns such as + item. Pro comparison and plan-selection copy must frame Patrol around what + it may handle automatically, issue investigation, policy-bounded action, + verified fixes, and history rather than as an abstract governed operator or + a choice about how much control Patrol has. + When free-plan copy mentions Patrol, it must describe watch-only/background + health checks; investigation, approval-backed fixes, autonomous action, + outcome verification, and extended history are Pro value. + Legacy packaging nouns such as `incident memory`, `scheduled remediations`, and `execution audit trail` must likewise stay out of current v6 commercial copy unless Pulse ships a first-class product surface that makes those names truthful again. The raw entitlement key `ai_autofix` may remain the internal capability identifier, but owned plan cards, comparison rows, and upgrade reasons must present it - as safe remediation workflows rather than `Patrol Auto-Fix` or other - stronger automation-first labels. Pulse Account preview/handoff pricing + as Patrol fixing safe issues within the selected Patrol mode rather than + as `Patrol Auto-Fix` or an abstract remediation-workflow label. Pulse Account preview/handoff pricing copy, public plan docs, and the in-app self-hosted billing shell must share that same free-first ladder so release-candidate and public purchase paths do not drift into a separate commercial story. @@ -720,7 +921,7 @@ or other self-hosted uncapped continuity plans. also treat `compatibility_only` entries as non-marketed compatibility capabilities by returning only the generic pricing destination rather than a feature-specific paid upgrade URL. -26. Keep hosted trial-activation verifier source selection compile-time owned. +24. Keep hosted trial-activation verifier source selection compile-time owned. `pkg/licensing/trial_activation.go`, `pkg/licensing/trial_activation_public_key_override_dev.go`, and `pkg/licensing/trial_activation_public_key_override_release.go` together @@ -728,7 +929,7 @@ or other self-hosted uncapped continuity plans. builds may keep the local override path, but release builds must resolve the verifier from the embedded build-time source of truth instead of honoring `PULSE_HOSTED_MODE` or other runtime wiring. -27. Add or change the Cloud control-plane runtime, public signup lifecycle, +25. Add or change the Cloud control-plane runtime, public signup lifecycle, tenant health/reconciliation, control-plane metrics, email delivery, hosted handoff, hosted entitlement refresh, or hosted tenant reaping through `internal/cloudcp/` and `internal/hosted/`. Those paths must stay governed @@ -813,50 +1014,50 @@ or other self-hosted uncapped continuity plans. volume telemetry must not become runtime status, entitlement enforcement, customer-facing support context, or self-hosted Plans presentation. 10. Keep organization billing loading placeholders on the shared - `SettingsLoadingSkeleton` primitive. Cloud-paid owns billing and usage - semantics; frontend-primitives owns the loading skeleton animation, fill - tokens, and metric/progress/card placeholder shells. -10. Keep Stripe webhook idempotency state bounded in the control-plane - registry: `internal/cloudcp/registry/registry.go` may retain `stripe_events` - rows long enough to suppress duplicate deliveries and reclaim stale - in-flight work, but it must prune expired processed or abandoned rows so - webhook dedupe does not grow without bound on disk. -11. Keep the maintained Pulse Account portal bundle source-synced with - `internal/cloudcp/portal/frontend/`: any slice that changes the portal - frontend hash or emitted manifest must rebuild - `internal/cloudcp/portal/dist/build_manifest.json` and keep - `internal/cloudcp/portal/frontend_sync_test.go` green in the same change. -12. Before GA, treat infrastructure monitoring volume as unmetered: - monitored systems remain the canonical inventory grouping unit, but paid - value must come from optional extras, hosted convenience, business - workflow, support, or similar non-core surfaces rather than using - monitored-system volume itself as a paid gate. - Child-resource volume, including guest capacity, must follow the same - self-hosted rule instead of becoming a replacement paid gate for core - monitoring. - Monitored-system impact-preview copy must follow the same rule: ordinary - connection previews may describe count impact and grouping changes, but - they must not use capacity-style titles or slash-style quota summaries that - imply self-hosted monitoring volume is the product being sold. -13. Keep v6 billing state uncapped even when persisted state still carries - legacy commercial volume-limit keys: - `pkg/licensing/models.go`, - `pkg/licensing/service.go`, - `pkg/licensing/entitlement_payload.go`, - `pkg/licensing/billing_state_normalization.go`, - `pkg/licensing/database_source.go`, - `pulse-pro:license-server/v6_store.go`, and - `pulse-pro:license-server/v6_schema.go` must scrub stale - `max_monitored_systems` values for Community/free, Relay, Pro, Pro+, Pro - Annual, lifetime, eligible grandfathered recurring, Cloud, and MSP plan - labels before runtime-capability, entitlement, grant, lease, billing, or - browser-facing payloads are built. - The same boundary must merge sparse legacy, configured-plan, or manually - supplied feature lists with canonical recognized-tier defaults before - storage, API response, or grant signing so Lifetime, Pro, Pro+, and - grandfathered recurring customers cannot be downgraded to the partial - feature list carried by an old JWT or legacy plan row. -14. Keep self-hosted commercial funnel stage ownership out of the customer + `SettingsLoadingSkeleton` primitive. Cloud-paid owns billing and usage + semantics; frontend-primitives owns the loading skeleton animation, fill + tokens, and metric/progress/card placeholder shells. +11. Keep Stripe webhook idempotency state bounded in the control-plane + registry: `internal/cloudcp/registry/registry.go` may retain `stripe_events` + rows long enough to suppress duplicate deliveries and reclaim stale + in-flight work, but it must prune expired processed or abandoned rows so + webhook dedupe does not grow without bound on disk. +12. Keep the maintained Pulse Account portal bundle source-synced with + `internal/cloudcp/portal/frontend/`: any slice that changes the portal + frontend hash or emitted manifest must rebuild + `internal/cloudcp/portal/dist/build_manifest.json` and keep + `internal/cloudcp/portal/frontend_sync_test.go` green in the same change. +13. Before GA, treat infrastructure monitoring volume as unmetered: + monitored systems remain the canonical inventory grouping unit, but paid + value must come from optional extras, hosted convenience, business + workflow, support, or similar non-core surfaces rather than using + monitored-system volume itself as a paid gate. + Child-resource volume, including guest capacity, must follow the same + self-hosted rule instead of becoming a replacement paid gate for core + monitoring. + Monitored-system impact-preview copy must follow the same rule: ordinary + connection previews may describe count impact and grouping changes, but + they must not use capacity-style titles or slash-style quota summaries that + imply self-hosted monitoring volume is the product being sold. +14. Keep v6 billing state uncapped even when persisted state still carries + legacy commercial volume-limit keys: + `pkg/licensing/models.go`, + `pkg/licensing/service.go`, + `pkg/licensing/entitlement_payload.go`, + `pkg/licensing/billing_state_normalization.go`, + `pkg/licensing/database_source.go`, + `pulse-pro:license-server/v6_store.go`, and + `pulse-pro:license-server/v6_schema.go` must scrub stale + `max_monitored_systems` values for Community/free, Relay, Pro, Pro+, Pro + Annual, lifetime, eligible grandfathered recurring, Cloud, and MSP plan + labels before runtime-capability, entitlement, grant, lease, billing, or + browser-facing payloads are built. + The same boundary must merge sparse legacy, configured-plan, or manually + supplied feature lists with canonical recognized-tier defaults before + storage, API response, or grant signing so Lifetime, Pro, Pro+, and + grandfathered recurring customers cannot be downgraded to the partial + feature list carried by an old JWT or legacy plan row. +15. Keep self-hosted commercial funnel stage ownership out of the customer product runtime. The customer frontend must not contain `upgradeMetrics`, `conversionEvents`, or infrastructure onboarding metrics wrappers or call sites at all. In-app `Plans & Billing`, pricing, checkout, paywall, or @@ -867,7 +1068,7 @@ or other self-hosted uncapped continuity plans. Pulse must not infer those portal stages from referrer state, and the commercial service must keep those self-hosted handoffs on release track `v6` even while the public site remains on `v5` before GA. -15. Keep retired local commercial funnel reporting out of customer diagnostics, +16. Keep retired local commercial funnel reporting out of customer diagnostics, settings, and product startup: `internal/api/diagnostics.go`, `frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx`, `frontend-modern/src/components/Settings/diagnosticsModel.ts`, @@ -878,7 +1079,7 @@ or other self-hosted uncapped continuity plans. pricing/checkout, upgrade-metrics, or infrastructure-onboarding analytics fields, controls, routes, stores, or startup DB artifacts to normal product users. -16. Keep ordinary self-hosted v6 commercial prompts opt-in. Cloud-paid runtime +17. Keep ordinary self-hosted v6 commercial prompts opt-in. Cloud-paid runtime may keep checkout, activation, recovery, and support-only trial plumbing available for explicit handoffs and entitled installs, but default self-hosted browser surfaces must honor `presentationPolicy.hideUpgrade` @@ -889,18 +1090,18 @@ or other self-hosted uncapped continuity plans. `/api/license/commercial-posture` bootstrap while `presentationPolicy.hideUpgrade` is true; explicit self-hosted plan, activation/recovery, and hosted or prompt-allowed flows may still refresh the shared posture store. -17. Keep hosted and trial billing construction separate from retired hosted-AI +18. Keep hosted and trial billing construction separate from retired hosted-AI quickstart inventory: `pkg/licensing/trial_start.go` and hosted entitlement refresh paths may preserve historical billing fields for old state, but new trial or hosted workspaces must not mint quickstart credits or imply a managed-model allowance as part of the commercial contract. -18. Keep Cloud signup acquisition owned by the Cloud control plane and public +19. Keep Cloud signup acquisition owned by the Cloud control plane and public account surfaces, not by the ordinary self-hosted app shell. The default self-hosted frontend must not register unauthenticated `/cloud` or `/cloud/signup` routes, and self-hosted pricing fallbacks must route Cloud interest to Pulse Account/public Cloud ownership rather than rendering an in-product trial or checkout page. -19. Keep the `unlimited` feature key neutral in shared metadata. It is a +20. Keep the `unlimited` feature key neutral in shared metadata. It is a hosted/MSP capacity-policy marker, not a self-hosted monitoring-volume product promise. Runtime and generated feature catalogs must label it as hosted capacity policy, keep it hidden from self-hosted plan cards, and @@ -1681,8 +1882,8 @@ returning to the owned plan state through the runtime-owned activation callback. Pulse product routes keep ownership of license status, usage, and activation state; `Pulse Account` owns the commerce flow itself. That same self-hosted settings surface is -plan-owned rather than tier-owned: the navigation label is `Plans`, the -surface title is `Self-hosted plan`, and the canonical plan state must make +plan-owned rather than tier-owned: the navigation label and surface title are +`Plans & Billing`, and the canonical plan state must make the current tier plus capabilities immediately obvious so existing paid upgrades can confirm what their new key enabled without hunting through generic billing details. That capability-first summary must stay tier-specific @@ -1706,6 +1907,31 @@ surface must show an explicit success summary that names the active tier and the marquee capabilities now available on this instance, rather than falling back to a generic "activated" banner that leaves the user to infer what changed from the steady-state billing card alone. +For active Pulse Pro/Lifetime tiers with the private Pro runtime available, +both the current-plan summary and the activation-success body must make the +next setup decision explicit: set Patrol mode, then let Patrol investigate, +act within that policy, verify outcomes, and keep context/history. Completed +verified Patrol outcomes are telemetry on this surface, not a reason to replace +the plan action with a default `Patrol history` CTA; when no active Patrol work +or pending governed decision exists, the paid-plan action returns to +`Choose Patrol mode`. +When the activated entitlement includes the governed Pulse Intelligence +operations-loop capability, that success summary may hand the user directly to +Patrol with a "Choose Patrol mode" action. The cloud-paid presentation contract +must name the action URL and action intent as Patrol-control concepts +(`PATROL_CONTROL_STARTER_URL` and `patrol_control`) rather than exposing +operations-loop names in the Plan surface. That action is still +cloud-paid-owned activation UX, but its destination must use the route-owned +Patrol control target +(`/patrol?patrolControlStarter=patrol_control#patrol-control`) so Patrol +owns starter recording through the shared Pulse Intelligence starter contract. +The legacy `patrol_autonomy` and `pulse_pro_activation` route and activity +markers remain compatibility input only, and the shared +`pulse_operations_loop` telemetry workflow marker may remain behind the Patrol +control surface. The Plans surface must not create a commercial-only analytics +counter, must not record prompt text or resource context, and must not treat the +click as proof that Patrol found an issue, Assistant explained it, a governed +action was approved, or the outcome was verified. That same router-owned billing contract now also includes recovery as a plan detail state instead of a fragment alias. The canonical recovery arrival is `/settings/system/billing/plan?details=recovery`, while @@ -1827,7 +2053,7 @@ commercial surface differently. That same shared presentation owner also carries the canonical cross-surface referral copy used outside the billing shell itself. When infrastructure or other adjacent settings surfaces need to point operators toward Plans & Billing -for billing, license status, or paid feature activation, they must consume the +for billing, license status, or paid feature access, they must consume the settings-owned referral strings from `frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts` instead of drafting route-local commercial guidance or reaching directly into @@ -1838,7 +2064,7 @@ is the canonical settings-shell adapter for self-hosted Plans & Billing shell framing and referral copy, while `frontend-modern/src/utils/licensePresentation.ts` remains the shared commercial notice/label owner for activation, trial, purchase, and recovery -language such as `License or Activation Key`. Hosted settings surfaces must +language such as `License key`. Hosted settings surfaces must not import self-hosted billing framing straight from the generic helper module when doing so would reintroduce top-level bundle-init cycles into hosted tenant settings routes. @@ -1886,8 +2112,8 @@ the no-cap monitored-system model as well. `ProLicensePanel.tsx`, self-hosted packages as core monitoring included in every self-hosted tier plus plan-specific extras: Community stays free for core monitoring, Relay adds remote access/Pulse Mobile handoff/push convenience and 14-day history, Pro adds Relay plus -AI operations, automation, root-cause analysis, safe remediation, advanced -administration, and 90-day history, while Pro+ remains legacy continuity only. +hands-on Patrol modes, issue investigation, verified fixes, advanced administration, +and 90-day history, while Pro+ remains legacy continuity only. Cloud/MSP pricing semantics stay separate, and grandfathered v5 continuity copy must describe legacy continuity and recorded baselines rather than current self-hosted monitored-system caps, capacity, or policy boundaries. @@ -1921,11 +2147,11 @@ self-hosted plan surface should imply monitored-system volume is a paid tier. That same pricing boundary now also owns the shared frontend plan-definition models. `frontend-modern/src/utils/cloudPlans.ts` and `frontend-modern/src/utils/selfHostedPlans.ts` are the canonical frontend -sources for cloud and self-hosted plan copy, limits, and comparison metadata, -while `frontend-modern/src/pages/CloudPricing.tsx`, +sources for cloud and self-hosted plan copy, limits, and comparison metadata. +`frontend-modern/src/pages/CloudPricing.tsx`, `frontend-modern/src/pages/HostedSignup.tsx`, and the self-hosted billing -settings surfaces must consume those shared owners instead of redefining -retail plan facts or counted-unit policy locally. +settings surfaces must consume those shared plan-definition sources instead of +redefining retail plan facts or counted-unit policy locally. That shared ownership also includes display-ready price semantics. Monthly headline price, founding-rate override, compare-at strike-through copy, campaign badge copy, and annual summary text must come from the shared @@ -2012,8 +2238,8 @@ for that feature's owning subsystem — those are user-initiated discovery paths, not blanket funnels, and are not required to be removed. Public AI and entitlement docs must use the same boundary: Community/Relay may describe Patrol background findings with BYOK, while investigation, proposed -remediation, safe remediation execution, and higher autonomy remain paid -AI-operations features. +fixes, governed fix execution, and higher autonomy remain paid AI-operations +features. Those docs should describe moving between available modes, not tell readers to "upgrade" as part of an ordinary safety progression. That same retired counted-unit boundary also owns the disclosure rule for diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index f161956df..f09e8ea88 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -88,7 +88,6 @@ TLS floor in the dynamic config. 51. `scripts/install.sh` 52. `scripts/install-mcp.sh` 53. `scripts/install-mcp.ps1` -54. `cmd/pulse-mcp/` 55. `scripts/pulse-auto-update.sh` 56. `scripts/release_control/internal/record_rc_to_ga_rehearsal.py` 57. `scripts/release_control/record_rc_to_ga_rehearsal.py` @@ -288,6 +287,11 @@ TLS floor in the dynamic config. 1. Add or change deployment-type detection, update planning, or apply behavior through `internal/updates/` 2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, governed promotion metadata resolution, the canonical version file, operator-facing release packet content, prerelease feedback intake wording, historical published-release integrity backfill, release asset validation status publication, download endpoint checksum/signature header proof, end-to-end install.sh smoke against the published release, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/release_asset_common.sh`, `scripts/backfill-release-assets.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/validate-release.sh`, `scripts/validate-published-release.sh`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/backfill-release-assets.yml`, `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/install-sh-smoke.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, and `.github/workflows/validate-release-assets.yml` + Release-facing agent-paradigm blurbs under `docs/releases/` must describe + `pulse-mcp` as a generic MCP adapter for MCP-speaking clients, not a + client-specific release artifact, and full-surface token guidance must come + from the manifest-owned `requiredScopes` list so release notes cannot drift + away from the shipped adapter. The `install-sh-smoke.yml` workflow runs end-to-end against the published release in a privileged systemd container: it downloads `install.sh` and `install.sh.sshsig` from the GitHub Release URL, @@ -397,6 +401,13 @@ TLS floor in the dynamic config. workspace package and Makefile must delegate their lab-agent targets through those same repo-root npm wrappers rather than duplicating raw launcher commands or teaching developers to paste one-off environment strings. + After a developer explicitly starts lab-agent mode, the managed and + foreground hot-dev launchers may remember that local workspace opt-in in + ignored `tmp/` state so ordinary managed restarts do not silently strand + already-installed LAN agents or lose the Proxmox guest Docker inventory + flags. Clean checkouts must still default to loopback-only development, and + an explicit `PULSE_DEV_LAB_AGENTS=false` / `PULSE_DEV_LAN=false` launch must + clear the remembered opt-in and return the workspace to local-only binding. The hot-dev supervisor must also recover its managed PID file from a live `hot-dev-bg.sh supervise` process before treating the runtime as unmanaged. Backend health monitoring must distinguish HTTP startup grace from a missing @@ -1149,8 +1160,9 @@ shell and Windows installers also sit on the shared agent-lifecycle boundary. `scripts/install-mcp.sh` and `scripts/install-mcp.ps1` extend the installer family with a fourth entry point: a stdio MCP server adapter (`cmd/pulse-mcp/`) that integrators run on their own -machine to drive Pulse from Claude Desktop, Claude Code, or other -MCP-speaking clients. The installers fetch a published +machine to expose the same Pulse Intelligence capability manifest +used by Pulse Assistant to OpenCode, Claude Desktop, Claude Code, or +other MCP-speaking clients. The installers fetch a published `pulse-mcp--` binary from the latest GitHub Release, verify SHA256 against the same `checksums.txt` the rest of the release uses, and place the binary at `~/.local/bin/pulse-mcp` @@ -1169,6 +1181,98 @@ installers consume. macOS notarization is intentionally skipped for v1: the README documents the Gatekeeper bypass and the install-script flow downloads the same unsigned binary, with the audit trail of SHA256 verification preserved. +The adapter's complete request/response tool-list projection, manifest +projection, capability and governance metadata formatting, request/response +tool filtering, typed input-schema projection, and API route/body call +projection semantics remain owned by `ai-runtime` and `api-contracts`; +deployment-installability owns building, publishing, installing, and launching +the same binary. README guidance may describe the manifest-provided typed +`inputSchema` arguments that MCP clients receive, including operator-state, +finding, and action tools, but those schemas remain an API/AI contract rather +than an installer or release-asset behavior. +README and startup guidance may describe API-token setup for the installed +adapter, but the set and order of advertised token scopes must be derived from +the manifest-owned `internal/agentcapabilities.RequiredCapabilityScopeList` +helper or the README generator's Markdown projection over that helper, not from +a deployment-local hardcoded scope list. Packaging may ship that guidance, but +it must not become a second owner of which scopes the current Pulse +Intelligence surface requires. +README guidance may also describe client setup for the installed adapter, but +server name, command, base URL flag/default, token environment variable, and +supported config families must be derived from +`internal/agentcapabilities.MCPClientConfigMarkdown` over +`Manifest.MCPAdapter`, not from deployment-local OpenCode, Claude, or +`pulse-mcp` setup snippets. Packaging may ship the generated prose and +installers, but it must not become a second owner of MCP client configuration. +Patrol finding tool scopes follow the same boundary: release assets may ship +the generated guidance, but the `ai:execute` requirement for Patrol finding +review and lifecycle calls comes from the manifest/API authorization contract, +not deployment-local monitoring-scope wording. +README guidance may also describe MCP workflow prompts, but the prompt +inventory must be derived from the shared `internal/agentcapabilities` +`ProjectPulseWorkflowPrompts` / `MCPPromptInventoryMarkdown` path. Packaging +may ship the generated prose, but it must not carry a deployment-local prompt +catalog. +README guidance may also describe capability-specific stable error codes, but +the error-code inventory must be derived from the shared +`internal/agentcapabilities` manifest through `MCPErrorCodeInventoryMarkdown`. +Packaging may ship the generated prose, but it must not carry a +deployment-local error-code catalog. +The shared manifest declaration and wire type in `internal/agentcapabilities/` +follow the same split: deployment-installability may package `cmd/pulse-mcp`, +but it must not fork or reinterpret the capability schema, shared +`ProjectedTool`/`ProjectTools` projection, shared `FindCapability` / +`ResolveCapability` lookup contract, shared named capability HTTP execution, +shared structured tool schema, provider-projection helpers, schema-envelope +helper, typed action-mode, approval-policy, or stable error-code contract +locally. +The shared event vocabulary follows that same split. Deployment-installability +may document and package MCP notification support, but event names advertised +by `cmd/pulse-mcp`, the `subscribe_events` manifest description, and +transport-event filtering, SSE record parsing, SSE-to-MCP notification +bridging, and MCP notification method projection remain owned by +`internal/agentcapabilities` plus the API/AI +contracts; release/install artifacts must not carry a separate event registry +or stream parser. The same boundary owns the event-stream HTTP subscription +primitive, including the `Accept: text/event-stream` request convention and +subscribe status handling; packaging may launch `cmd/pulse-mcp` but must not +fork that transport or notification-bridge behavior into install scripts or +release artifacts. +The shared MCP JSON-RPC, request decoding, line-delimited stdio request +serving, notification response policy, stable JSON-RPC encoding, +manifest-backed tool-server semantics, tool-server method dispatch, +initialize instruction/tool-call/resource/prompt payloads, `tools/call` params decode, tool-call +parameter normalization/validation, tool-server initialize result construction, +capability lookup translation, named HTTP invocation, MCP resource URI +projection, context-backed `resources/list` / `resources/read` projection, +manifest-backed `prompts/list` / `prompts/get` workflow prompt projection, and +result envelopes follow the same boundary. Deployment-installability may +document, package, and launch +`cmd/pulse-mcp`, but protocol versions, JSON-RPC error codes, +event-notification projection, method payload JSON, initialize +operating-instruction projection, MCP content/result JSON, +request decoding, line-framing loops, JSON-RPC response serialization, +notification response policy, SSE-to-MCP notification bridging, +manifest-backed tool handlers, method dispatch, initialize response +construction, the MCP tools/call raw bridge, neutral capability tool HTTP +execution, resource URI construction, context-capability resource projection, +prompt catalog and rendering, prompt-argument validation, `HTTPCallResponse` to +shared tool-result wrapping, text/marker interpretation, and the shared rule +that trims/requires tool names while cloning/initializing argument maps must +remain owned by `internal/agentcapabilities` so the installed adapter cannot +drift from Assistant method, tool-call parameter, resource, prompt, or +tool-result execution. +The same split applies to governed Assistant tool markers: packaging may +document approval-required and policy-blocked outcomes, but marker prefixes, +payload `type` values, formatting, and parsing remain owned by +`internal/agentcapabilities`. +The shared agent HTTP substrate follows the same boundary: +deployment-installability may describe how to pass a token and base URL to the +installed adapter, but manifest fetch paths, API-token header spelling, request +content-type behavior, capability HTTP execution, request/response body-return +helpers, status-derived MCP `isError` behavior, and stable non-2xx +error-envelope formatting remain owned by +`internal/agentcapabilities`. That same installer boundary now owns instance identity for side-by-side server installs too: the root `install.sh`, generated update helper, and `scripts/pulse-auto-update.sh` must preserve an explicitly selected service diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 3cf83c6bd..2b1529447 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -9,10 +9,7 @@ "contract_file": "docs/release-control/v6/internal/subsystems/frontend-primitives.md", "status_file": "docs/release-control/v6/internal/status.json", "registry_file": "docs/release-control/v6/internal/subsystems/registry.json", - "dependency_subsystem_ids": [ - "agent-lifecycle", - "storage-recovery" - ] + "dependency_subsystem_ids": ["agent-lifecycle", "cloud-paid", "storage-recovery"] } ``` @@ -29,7 +26,7 @@ work extends shared components instead of creating new local variants. 4. `frontend-modern/src/components/Settings/SettingsPageShell.tsx` 5. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts` 6. `frontend-modern/src/components/Settings/APIAccessPanel.tsx` -6a. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` + 6a. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` 7. `frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx` 8. `frontend-modern/src/components/Settings/AIModelSelectionSection.tsx` 9. `frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx` @@ -66,7 +63,7 @@ work extends shared components instead of creating new local variants. 40. `frontend-modern/src/utils/systemSettingsPresentation.ts` 41. `frontend-modern/src/utils/aiSettingsPresentation.ts` 42. `frontend-modern/src/utils/settingsShellPresentation.ts` -42a. `frontend-modern/src/i18n/` + 42a. `frontend-modern/src/i18n/` 43. `frontend-modern/src/utils/textPresentation.ts` 44. `frontend-modern/src/components/Settings/UpdateInstallGuide.tsx` 45. `frontend-modern/src/components/Settings/updatesSettingsModel.ts` @@ -84,11 +81,11 @@ work extends shared components instead of creating new local variants. 57. `frontend-modern/src/components/shared/FilterBar/AddFilterMenu.tsx` 58. `frontend-modern/src/components/shared/FilterBar/filterCatalog.ts` 59. `frontend-modern/src/components/shared/FilterBar/index.ts` -59a. `frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx` -59b. `frontend-modern/src/components/shared/FilterBar/useSavedViews.ts` -59c. `frontend-modern/src/components/shared/FilterBar/filterOptionPresentation.tsx` + 59a. `frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx` + 59b. `frontend-modern/src/components/shared/FilterBar/useSavedViews.ts` + 59c. `frontend-modern/src/components/shared/FilterBar/filterOptionPresentation.tsx` 60. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts` -61. `frontend-modern/src/features/` +61. `frontend-modern/src/features/` (including Patrol presentation, where Patrol control starter counts are context only even when mirrored through `patrolAutonomy*` compatibility fields, successful direct Patrol control saves may record the content-free `patrol_control` starter only when paid control is available and the effective control posture changes and must then refresh Patrol status, findings, approvals, and run history before the operator waits for polling, legacy `proActivation*` starter aliases must not render a separate proof strip by themselves, Patrol control completed/resolved counts may only project backend-owned terminal proof, current active findings and pending approvals outrank historical completion proof in the primary operator state, selected run history must read as a Patrol run record rather than a findings filter or snapshot workflow, terminal verified/rejected outcomes with no active finding or pending approval must stay history detail without rendering a no-op proof strip, resolved-only issue history must not be promoted into current-work copy or actions, compact recurrence/trust counters must read as historical evidence rather than current issue state, Patrol-owned status/history evidence must keep the assessment visible when the broader intelligence summary is missing, the default Patrol workspace must lead with current work while run history stays a deliberate secondary review surface, setup-only Patrol runtime failures must instead use `Fix Patrol setup` framing with a dedicated setup task and direct provider-settings action while suppressing generic issue-row chips and filter chrome, Patrol must not expose a generic Details/supporting-context panel for nearby activity, related patterns, or policy limits, locked-control copy must state the watch-only boundary in positive capability language by saying Patrol watches infrastructure and shows current issues, avoid repeating the same sentence across the header and control, and avoid repeatedly restating infrastructure-unchanged caveats or relying on disabled controls, compact Pro badges, Limits controls, or manual-review framing, `patrolControlValueState` decides whether a terminal decision is partial review context or verified value proof while `patrolAutonomyValueState` remains a compatibility mirror, legacy `proActivation*` fields are compatibility fallback only, native Patrol state must not load the operations-loop status projection to decide current work, local Patrol state must expose issue-backed `patrolWork*` evidence rather than legacy proof naming, Patrol mode labels remain domain copy that must describe backend-owned risk policy without creating page-local safety thresholds, the Patrol schedule and model drawer must stay separate from the always-visible Patrol mode selector instead of duplicating the four control choices or reintroducing save/apply configuration framing, and Patrol header refresh controls must call an explicit operator-refresh handler whose spinning/disabled state is separate from background polling and initial data loads) 62. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx` 63. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` 64. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx` @@ -144,8 +141,8 @@ work extends shared components instead of creating new local variants. 114. `frontend-modern/src/components/shared/useUpgradeNavigation.ts` 115. `frontend-modern/src/utils/upgradeNavigation.ts` 116. `frontend-modern/src/components/DemoBanner.tsx` -116a. `frontend-modern/src/components/CommercialMigrationBanner.tsx` -116b. `frontend-modern/src/components/GitHubStarBanner.tsx` + 116a. `frontend-modern/src/components/CommercialMigrationBanner.tsx` + 116b. `frontend-modern/src/components/GitHubStarBanner.tsx` 117. `frontend-modern/src/components/Login.tsx` 118. `frontend-modern/src/stores/sessionCapabilities.ts` 119. `frontend-modern/src/stores/sessionPresentationPolicy.ts` @@ -161,9 +158,9 @@ work extends shared components instead of creating new local variants. 129. `frontend-modern/scripts/shared-template-audit.mjs` 130. `frontend-modern/scripts/shared-template-registry.json` 131. `frontend-modern/src/features/platformPage/sharedPlatformPage.tsx` -131a. `frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx` -131b. `frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx` -131c. `frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx` + 131a. `frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx` + 131b. `frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx` + 131c. `frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx` 132. `frontend-modern/src/utils/platformSupportManifest.generated.ts` 133. `frontend-modern/src/utils/platformSupportManifest.ts` 134. `frontend-modern/src/utils/sourcePlatformOptions.ts` @@ -172,10 +169,10 @@ work extends shared components instead of creating new local variants. 137. `frontend-modern/src/components/shared/Button.tsx` 138. `frontend-modern/src/components/shared/buttonModel.ts` 139. `frontend-modern/src/components/shared/Button.test.tsx` -139a. `frontend-modern/src/components/shared/InlineNotice.tsx` -139b. `frontend-modern/src/components/shared/InlineNotice.test.tsx` -139c. `frontend-modern/src/components/shared/ExternalTextLink.tsx` -139d. `frontend-modern/src/components/shared/ExternalTextLink.test.tsx` + 139a. `frontend-modern/src/components/shared/InlineNotice.tsx` + 139b. `frontend-modern/src/components/shared/InlineNotice.test.tsx` + 139c. `frontend-modern/src/components/shared/ExternalTextLink.tsx` + 139d. `frontend-modern/src/components/shared/ExternalTextLink.test.tsx` 140. `frontend-modern/src/components/shared/CopyableCodeRow.tsx` 141. `frontend-modern/src/components/shared/DetailSectionTable.tsx` 142. `frontend-modern/src/components/shared/detailSectionModel.ts` @@ -184,6 +181,13 @@ work extends shared components instead of creating new local variants. ## Shared Boundaries +Settings navigation discoverability is part of the shared settings-shell +boundary. A settings route that is available in normal commercial presentation +must be reachable through the sidebar unless it is explicitly a hidden +deep-link flow. The self-hosted `Plans` page is not a deep-link-only flow: +`system-billing` stays visible whenever commercial presentation is allowed, +while demo or commercial-suppressed sessions may still hide it. + Frontend localization is a shared primitive boundary. Locale support must flow through typed message catalogs with an English fallback and explicit seed locale coverage rather than page-local string switches. @@ -198,9 +202,20 @@ settings, first-run, empty-state, and alert copy may be localized, but machine-facing values must remain stable: commands, environment variables, API fields, config keys, log lines, error codes, hostnames, resource names, product identifiers, and vendor object names stay untranslated unless the owning -runtime contract explicitly says otherwise. Migrated settings surfaces must -render customer-facing copy through the catalog and shared presentation helpers -rather than reintroducing panel-local English. Migrated first-session surfaces, +runtime contract explicitly says otherwise. Shared settings-shell header copy +for the self-hosted plan must keep first-wave locale catalogs aligned with the +English product stance: on Pro, the operator chooses how autonomous Patrol +should be. The settings shell must not teach a separate activation loop, MCP +readiness, or operations-loop proof model as the default plan setup story. +Pulse Intelligence external-agent setup uses the same shell language with a +`Choose Patrol mode` handoff before scoped-token setup, and the expanded setup +checklist must say to set how autonomous Patrol should be before connected +agents request work rather than repeating internal automation/proof wording. +Developer-only external-agent posture uses `External agents` plus Patrol mode +before surfacing MCP or workflow prompt wire names. +Migrated settings surfaces must render customer-facing copy through the catalog +and shared presentation helpers rather than reintroducing panel-local English. +Migrated first-session surfaces, including the Setup Wizard shell, welcome/security steps, setup completion handoff, and runtime-home loading handoff, follow the same catalog path; their guardrails must fail if the migrated journey returns to page-local English or @@ -385,88 +400,183 @@ attention/running/other filters so large bridge or overlay networks remain scan-friendly without hiding any container from drilldown. 1. `frontend-modern/src/components/CommercialMigrationBanner.tsx` shared with `cloud-paid`: the global commercial migration notice is both a cloud-paid entitlement recovery surface and a shared app-shell notice primitive consumer. -2. `frontend-modern/src/components/Settings/APIAccessPanel.tsx` shared with `security-privacy`: the API Access settings intro is both a security/privacy token-management trust surface and a canonical settings-shell presentation boundary. +2. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` shared with `ai-runtime`, `api-contracts`: the External agents settings panel is the optional settings-shell projection of Pulse MCP onboarding, the AI runtime connected-agent onboarding surface, and a presentation consumer of the shared agent capabilities frontend client. +3. `frontend-modern/src/components/Settings/APIAccessPanel.tsx` shared with `security-privacy`: the API Access settings intro is both a security/privacy token-management trust surface and a canonical settings-shell presentation boundary. The panel may own shell placement and local action layout, but token-specific Docker / Podman copy must come from `frontend-modern/src/utils/apiTokenPresentation.ts` rather than page-local text. -3. `frontend-modern/src/components/Settings/DataHandlingPanel.tsx` shared with `security-privacy`: the data-handling settings surface is both a security/privacy trust surface and a canonical settings-shell presentation boundary. -4. `frontend-modern/src/components/Settings/dataHandlingPanelModel.ts` shared with `security-privacy`: the data-handling settings model is both a security/privacy posture projection and a canonical settings-shell presentation boundary. -5. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` shared with `security-privacy`: the general settings privacy panel is both a security/privacy control surface and a canonical settings-shell presentation boundary. + `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` stays + under Pulse Intelligence Assistant settings because connected agents are an + optional access path to Patrol work, while API Access remains the token + minting surface linked from setup. Its setup copy must present one shared + `pulse-mcp` runtime contract with client-native wrappers: OpenCode's top-level + `opencode.json` / `mcp` shape and the common `mcpServers` shape for + Claude-style clients. The server name, command, base URL flag/default, + token environment variable, supported config families, and copied config + snippets must flow through `frontend-modern/src/api/agentCapabilities.ts` + from `/api/agent/capabilities.mcpAdapter`; the component may arrange and + label them, but must keep setup mechanics, raw client config snippets, and + developer details behind deliberate disclosures so the default Assistant + settings view stays focused on chat command access and optional external + access rather than raw JSON. External-agent posture should present + `External agents` as the visible surface and reserve `Pulse MCP` or + `pulse_operations_loop` for wire-name/debugging detail. The Patrol control handoff must route to the + Patrol operator surface where the inline control level is configured. When + setup is opened, the setup order must put Patrol control before scoped-token + creation and client connection, while installer commands, client config + snippets, and developer details remain deliberate disclosures. + It must not frame + Pulse MCP as a Claude-only surface, force OpenCode through a Claude-style + wrapper, or duplicate a client-specific tool list. Full-surface token guidance in that panel must render the manifest-provided + `requiredScopes` list through + `frontend-modern/src/api/agentCapabilities.ts`; it must not hardcode a + partial scope set in browser copy. Capability category order, labels, and + descriptions must also come from the same manifest client; the settings + panel may provide compatibility fallback rendering through that client, but + it must not own a local category presentation table or + `/api/agent/capabilities` fetcher. The panel's Pulse Intelligence surface + summary is the same manifest projection: Pulse Intelligence Core, Patrol, + Assistant, and MCP labels/descriptions plus surface affordance badges must flow through + `frontend-modern/src/api/agentCapabilities.ts` from + `/api/agent/capabilities.surfaceContract`, leaving the component to own only + settings-shell layout. The visible external-adapter label in onboarding copy + must also come from that manifest-derived capability posture instead of a + hard-coded panel-local product label, so the settings shell cannot drift from + the published agent surface contract. MCP capability-posture chips in that same panel must also + flow through `getAgentManifestSurfaceToolContract(manifest, +AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`, + with the static inventory read only from + `/api/agent/capabilities.surfaceToolContracts`; missing + `surfaceToolContracts` entries must not make the browser infer MCP tools + from raw capabilities. The panel must not own request / response capability + filtering, call a per-surface projection alias, know the `subscribe_events` + streaming exception, or maintain a local MCP tool count. The shared frontend + manifest client must keep MCP onboarding on the generic surface resolver + rather than exporting a Pulse-MCP-specific tool-contract helper. The panel's + settings shell may arrange the external-agent setup hierarchy, but it must + keep connected agents framed as optional access to Pulse context and Patrol + work, with Patrol as the built-in operator that watches, acts within Patrol + mode, asks when approval is required, verifies outcomes, and records + history, state that external agents use that same boundary, and make the canonical + `/settings/pulse-intelligence/assistant#external-agent-setup` route land on + and briefly focus this panel with setup open rather than leaving the user at + the generic API token inventory. Legacy `/settings/security/api#external-agent-setup` and + `/settings/security/api#pulse-mcp-setup` links must remain accepted and may + redirect to the canonical Pulse Intelligence Assistant route. Normal API + Access visits remain token-management first, and external-agent setup must + not reorder the API token inventory because it no longer lives on that page. + The Assistant settings default must keep setup mechanics behind a `Show connector setup` + disclosure so it does not introduce a separate external-agent operator + journey or make copied MCP config blocks or tool-contract proof badges the + default visual weight of Pulse Intelligence settings. Its Developer details + disclosure may show posture and policy summaries behind Patrol + access model, but prompt, scope, failure-code, and tool inventories must sit + behind a nested Live manifest details disclosure so the advanced surface + remains navigable. The panel's + manifest-owned `pulse_operations_loop` prompt row may expose the stable + prompt id for client builders, but its visible label, description, and badge + must keep the user-facing Patrol framing from the manifest instead + of reintroducing operations-loop proof wording in the settings shell. The panel's + explanatory onboarding copy must name published manifest-owned surface + contracts as the publication boundary rather than suggesting raw backend + capabilities become visible automatically. + The visible token preset name in that setup must be `Patrol external agent`, + not `Pulse Intelligence agent`; the latter may survive only as a route/model + compatibility id. + Manifest-backed stable failure-code + summaries may use settings-shell chips or compact lists, but code selection + and capability attribution stay owned by the API manifest client. Token + setup handoff buttons or anchors in + the Agent integrations panel are settings-shell chrome only: they may route + to the API Access token creation section, but token preset semantics and + required-scope derivation remain owned by the API/security boundary. +4. `frontend-modern/src/components/Settings/DataHandlingPanel.tsx` shared with `security-privacy`: the data-handling settings surface is both a security/privacy trust surface and a canonical settings-shell presentation boundary. +5. `frontend-modern/src/components/Settings/dataHandlingPanelModel.ts` shared with `security-privacy`: the data-handling settings model is both a security/privacy posture projection and a canonical settings-shell presentation boundary. +6. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` shared with `security-privacy`: the general settings privacy panel is both a security/privacy control surface and a canonical settings-shell presentation boundary. The panel owns compact settings-shell framing for anonymous telemetry, but its vocabulary must stay aligned with `security-privacy`: aggregate - self-hosted adoption counts and coarse feature flags may be named, while + self-hosted adoption counts, coarse feature flags, and coarse Patrol, + Assistant, and external-agent usage counters may be named, while hostnames, credentials, infrastructure identifiers, prompts, chat messages, - and personal information must stay explicitly excluded. -6. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` shared with `security-privacy`: the authentication settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. -7. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` shared with `security-privacy`: the security overview settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. -8. `frontend-modern/src/routing/routePreload.ts` shared with `performance-and-scalability`: the app-shell route preload registry is both a canonical frontend shell boundary and an authenticated hot-path performance boundary. -9. `frontend-modern/src/stores/aiChat.ts` shared with `ai-runtime`: the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary. - Assistant session pickers and reloads must restore only safe - `handoff_summary` presentation state from the session list. Loading a plain - session or starting a new conversation must clear stale scoped handoff - briefing state so Patrol and alert context does not visually leak between - conversations. Browser-originated model handoff payloads are one-shot - request seeds: after the first successful chat send, this store must clear - `handoffContext`, `handoffResources`, `handoffActions`, and safe - `handoffMetadata` while preserving the safe visible briefing and scoped - approval-required posture, so later turns rely on backend session hydration - instead of resending stale browser context. Patrol handoffs must not include - Persisted Assistant redo availability is safe session chrome, not transcript - content. The drawer may consume `ChatSession.can_redo` from the backend - session list to re-enable Redo after reload or session refresh, but it must - not read or duplicate the redo stack in frontend state. Undo restores the - editable prompt draft and safe structured send metadata into the composer; - Redo clears that recovered draft only after the backend restores the turn. - Patrol handoffs must not include - safe next-step labels, action kinds, or whitelisted app-route hrefs in - `handoffMetadata`; the drawer must treat - `handoff_summary.requires_approval` as a current pending-decision flag, not a - historical action marker, so completed or rejected handoff actions render as - action context rather than pending approval. A restored Patrol run summary - must remain visibly sourced to Pulse Patrol, restore a `patrol-run` target - plus run ID/type/status/runtime-failure presentation only, and must not - rehydrate model-only runtime failure detail into browser context. New Patrol - run requests follow the same drawer boundary: source-owned context and - briefing copy may show classified, redacted failure summaries for operator - review, but `handoffContext`, `handoffResources`, and `handoffActions` for - run-history context must stay absent from the browser request so the backend - can rebuild model-bound context from the stored Patrol run. Restored - Patrol assessment, Patrol finding, and Patrol configuration-failure sessions - follow the same safe-summary rule: the drawer may restore source label, - title, target type, status badge, bounded resource facts, and approval/action - status from `handoff_summary`, but - it must not infer a finding target from bounded action references or - reconstruct hidden model context, provider details, retry payloads, commands, - preflight output, or action results in the browser. If the safe summary - was created by a legacy build that stored Patrol next-step metadata, - recommendation detail, action labels, safe action kind, or whitelisted - app-route href, the session picker plus restored drawer must ignore those - fields rather than carrying them forward as hidden context or visible - recommendation copy. - Session-load and new-conversation transitions must be success-bound: if the - underlying session operation fails, the shared drawer store must not clear or - replace the current scoped handoff context. - Live Patrol assessment drawer opens must use that same - `patrol-assessment`/`pulse-patrol-assessment` target identity rather than a - retired dashboard target, so first-open and restored-session chrome remain - source-named. - The shared `frontend-modern/src/components/shared/AIModelPicker.tsx` - primitive must keep model route presentation delegated to the AI runtime - label helpers. Pulse-owned local Assistant routes such as - `pulse:local-inventory` and `pulse:mock-assistant` are implementation - routes and must render as named choices without secondary raw route IDs, - while external provider route IDs may remain visible where they disambiguate - catalog entries. -10. `frontend-modern/src/utils/platformSupportManifest.generated.ts` shared with `unified-resources`: the generated platform support projection is both a canonical unified-resource platform union boundary and a shared frontend source/platform vocabulary boundary. - It must expose the manifest `surface_kind` field so runtime lenses such as - `docker` are not collapsed back into owning platform semantics. - It must also preserve canonical projection lists from the governed manifest - without page-local narrowing; for example, TrueNAS exposes both native - `vm`, `network-share`, and `app-container` workloads through the same - generated platform projection used by route helpers, badges, source - filters, reportable-resource pickers, and type unions. -11. `frontend-modern/src/utils/sourcePlatforms.ts` shared with `unified-resources`: the source platform normalizer is both a canonical unified-resource source adapter boundary and a shared frontend source/platform vocabulary boundary. + command text, action output, token values, and personal information must + stay explicitly excluded. +7. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` shared with `security-privacy`: the authentication settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. +8. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` shared with `security-privacy`: the security overview settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. +9. `frontend-modern/src/routing/routePreload.ts` shared with `performance-and-scalability`: the app-shell route preload registry is both a canonical frontend shell boundary and an authenticated hot-path performance boundary. +10. `frontend-modern/src/stores/aiChat.ts` shared with `ai-runtime`: the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary. + Assistant session pickers and reloads must restore only safe + `handoff_summary` presentation state from the session list. Loading a plain + session or starting a new conversation must clear stale scoped handoff + briefing state so Patrol and alert context does not visually leak between + conversations. Browser-originated model handoff payloads are one-shot + request seeds: after the first successful chat send, this store must clear + `handoffContext`, `handoffResources`, `handoffActions`, and safe + `handoffMetadata`; it must also clear any preferred workflow prompt request + once the manifest-rendered starter has seeded the composer and the first + scoped send succeeds. The store must preserve the safe visible briefing and + scoped approval-required posture, so later turns rely on backend session + hydration instead of resending stale browser context. Patrol handoffs must not include + Persisted Assistant redo availability is safe session chrome, not transcript + content. The drawer may consume `ChatSession.can_redo` from the backend + session list to re-enable Redo after reload or session refresh, but it must + not read or duplicate the redo stack in frontend state. Undo restores the + editable prompt draft and safe structured send metadata into the composer; + Redo clears that recovered draft only after the backend restores the turn. + Browser-owned session-management comments, command request handling, and + question-answer plumbing in this store and the adjacent chat hook must name + the native drawer surface as Pulse Assistant rather than reviving the + retired generic `Pulse AI` label. Shared model/provider settings guidance + still belongs to Pulse Intelligence > Provider & Models. + Patrol handoffs must not include + safe next-step labels, action kinds, or whitelisted app-route hrefs in + `handoffMetadata`; the drawer must treat + `handoff_summary.requires_approval` as a current pending-decision flag, not a + historical action marker, so completed or rejected handoff actions render as + action context rather than pending approval. A restored Patrol run summary + must remain visibly sourced to Pulse Patrol, restore a `patrol-run` target + plus run ID/type/status/runtime-failure presentation only, and must not + rehydrate model-only runtime failure detail into browser context. New Patrol + run requests follow the same drawer boundary: source-owned context and + briefing copy may show classified, redacted failure summaries for operator + review, but `handoffContext`, `handoffResources`, and `handoffActions` for + run-history context must stay absent from the browser request so the backend + can rebuild model-bound context from the stored Patrol run. Restored + Patrol assessment, Patrol finding, and Patrol control save-failure sessions + follow the same safe-summary rule: the drawer may restore source label, + title, target type, status badge, bounded resource facts, and approval/action + status from `handoff_summary`, but + it must not infer a finding target from bounded action references or + reconstruct hidden model context, provider details, retry payloads, commands, + preflight output, or action results in the browser. If the safe summary + was created by a legacy build that stored Patrol next-step metadata, + recommendation detail, action labels, safe action kind, or whitelisted + app-route href, the session picker plus restored drawer must ignore those + fields rather than carrying them forward as hidden context or visible + recommendation copy. + Session-load and new-conversation transitions must be success-bound: if the + underlying session operation fails, the shared drawer store must not clear or + replace the current scoped handoff context. + Live Patrol assessment drawer opens must use that same + `patrol-assessment`/`pulse-patrol-assessment` target identity rather than a + retired dashboard target, so first-open and restored-session chrome remain + source-named. + The shared `frontend-modern/src/components/shared/AIModelPicker.tsx` + primitive must keep model route presentation delegated to the AI runtime + label helpers. Pulse-owned local Assistant routes such as + `pulse:local-inventory` and `pulse:mock-assistant` are implementation + routes and must render as named choices without secondary raw route IDs, + while external provider route IDs may remain visible where they disambiguate + catalog entries. +11. `frontend-modern/src/utils/platformSupportManifest.generated.ts` shared with `unified-resources`: the generated platform support projection is both a canonical unified-resource platform union boundary and a shared frontend source/platform vocabulary boundary. + It must expose the manifest `surface_kind` field so runtime lenses such as + `docker` are not collapsed back into owning platform semantics. + It must also preserve canonical projection lists from the governed manifest + without page-local narrowing; for example, TrueNAS exposes both native + `vm`, `network-share`, and `app-container` workloads through the same + generated platform projection used by route helpers, badges, source + filters, reportable-resource pickers, and type unions. +12. `frontend-modern/src/utils/sourcePlatforms.ts` shared with `unified-resources`: the source platform normalizer is both a canonical unified-resource source adapter boundary and a shared frontend source/platform vocabulary boundary. That shared boundary must preserve `availability` as the agentless monitoring source for `network-endpoint` resources and settings presets, so source badges and platform/source type resolution do not fall back to @@ -488,6 +598,27 @@ Feature surfaces under `frontend-modern/src/features/` may own product-specific assessment semantics, but they must keep those semantics in their governed presentation helpers and render them inside the shared neutral Pulse surface language rather than introducing page-local verdict bands or nested cards. +Feature-owned runtime hooks may also own non-visual side effects when those +effects are part of the governed feature workflow. For Patrol, current-work +action chrome must keep active findings in the Patrol findings workflow first; +Assistant handoffs stay contextual actions on selected findings, approvals, or +history records rather than the primary current-work CTA. When provider/model +readiness blocks manual Patrol, the feature header must render the shared +primary-action chrome as a Provider & Models setup link instead of a disabled +run button that looks primary but cannot act. The Patrol hook owns the +content-free `pulse_operations_loop` starter marker so render components do not +fork telemetry or privacy behavior. +When the shared operations-loop status projection reports contextual +Assistant/external-agent collaboration inside the Assistant step, Assistant or +Pulse MCP starter counts, Patrol control starter evidence, or Patrol control +completed-loop or resolved-loop proof, feature presentation helpers may render +those facts as compact title or step detail inside the existing journey layout. +They must read primary `patrolControl*` fields first, then fall back to legacy +`patrolAutonomy*` compatibility fields, and may fall back to legacy +`proActivation*` fields only when the Patrol-control projections are absent. +They must keep the neutral shared surface chrome stable and must not add +page-local badges, nested cards, or alternate progress widgets for the same +evidence. Feature-owned table drawers use shared disclosure and inline-detail primitives as local interaction state. Unless a surface has a separately governed deep-link write contract, opening or closing a row drawer must preserve the current @@ -603,6 +734,10 @@ not a replacement status card, CTA band, or page-local nested card. entitlement state, and click handlers, while `Button`, `ButtonLink`, and `UpgradeButtonLink` own the primary, outline, warning, and upgrade/link chrome. + Manual self-hosted key recovery is a secondary detail in that same boundary: + settings surfaces may expose the fallback, but they must label it as license + recovery/key recovery and keep normal checkout plus the Pro Patrol-mode setup + path ahead of recovery mechanics or activation-key terminology. Hosted billing admin organization row actions follow the same boundary: cloud-paid surfaces own Suspend, Activate, Reload, tenant state, and mutation semantics, while `Button` owns the row-action chrome through the secondary @@ -749,11 +884,11 @@ not a replacement status card, CTA band, or page-local nested card. Native select state belongs to the shared `frontend-modern/src/components/shared/FormSelect.tsx` primitive. It must apply controlled `value` props after options are mounted so settings panels - such as workload discovery show the persisted option instead of falling back + such as Discovery show the persisted option instead of falling back to the first option while the collapsed summary shows a different value. The Assistant runtime controls in `frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx` — e.g. the - workload discovery `Toggle` — are settings-shell chrome bound to the canonical + service context scan `Toggle` — are settings-shell chrome bound to the canonical `useAISettingsState` form and `/api/settings/ai` payload, not local browser state. Each must bind to a `state.form.*` field, round-trip through the field-by-field settings payload, and source its label, help, and summary copy @@ -761,7 +896,7 @@ not a replacement status card, CTA band, or page-local nested card. strings or reaching for a bespoke fetch outside the canonical payload. There is no cloud-context-privacy control here: cloud context behavior is a fixed posture (see `ai-runtime`), not an operator setting. -3. Add feature-specific presentation only when no shared primitive should own it. +2. Add feature-specific presentation only when no shared primitive should own it. Feature surfaces under `frontend-modern/src/features/` that display product labels must consume the owning subsystem's presentation utilities rather than hard-coding divergent page-local copy. Shared primitives and @@ -830,7 +965,7 @@ not a replacement status card, CTA band, or page-local nested card. hard-to-scan strip. Narrow consumers such as `ColumnPicker` must opt into their panel width through that primitive rather than layering competing width classes page by page. -4. Add guardrail tests when a new shared pattern is introduced. +3. Add guardrail tests when a new shared pattern is introduced. Shared monitored-system primitives must prove they remain informational grouping or ledger surfaces rather than admission-freeze banners, cap summaries, or `current / limit` quota math. @@ -848,7 +983,7 @@ not a replacement status card, CTA band, or page-local nested card. from a platform-owned product surface such as a recovery tab, the first highlighted step should match that route instead of always restarting at Dashboard. -5. Keep shared infrastructure shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` and `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx` must continue to derive provider counts and shared subtab copy from one infrastructure-settings source — via the unified aggregator through `frontend-modern/src/components/Settings/useConnectionsLedger.ts` — instead of creating provider-local summary fetches or VMware-only shell vocabulary. Phase 9 retired the old `PlatformConnectionsWorkspace` per-type shell; setup guidance should now use `Add infrastructure` plus source-strategy language for API-backed onboarding. The standalone connections-table presenter is retired; `frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx` is the only landing-ledger presenter for configured infrastructure rows, and it must exclude agentless availability probes because those belong to `frontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsx`. +4. Keep shared infrastructure shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` and `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx` must continue to derive provider counts and shared subtab copy from one infrastructure-settings source — via the unified aggregator through `frontend-modern/src/components/Settings/useConnectionsLedger.ts` — instead of creating provider-local summary fetches or VMware-only shell vocabulary. Phase 9 retired the old `PlatformConnectionsWorkspace` per-type shell; setup guidance should now use `Add infrastructure` plus source-strategy language for API-backed onboarding. The standalone connections-table presenter is retired; `frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx` is the only landing-ledger presenter for configured infrastructure rows, and it must exclude agentless availability probes because those belong to `frontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsx`. The first-run setup wizard inherits that same source-strategy vocabulary: step labels and completion copy must frame the final setup step as choosing the first infrastructure source, not installing a host. Successful token @@ -862,7 +997,7 @@ not a replacement status card, CTA band, or page-local nested card. and no redundant monitored-systems ledger beneath it. The landing route may include a dedicated first-viewport toolbar that explains platform APIs and Pulse Agent telemetry as infrastructure sources and exposes `Add - infrastructure`, `Run discovery`, and `Discovery settings` inside the +infrastructure`, `Run discovery`, and `Discovery settings` inside the source manager. Per-source add actions, including `Install Pulse Agent`, belong on the governed source rows, and `Detect address` stays inside the single API-platform probe path instead of a duplicate toolbar action. It may @@ -890,7 +1025,7 @@ not a replacement status card, CTA band, or page-local nested card. `useConnectionsLedger.ts`, `InfrastructureSourceManager.tsx`, and `InfrastructureWorkspace.tsx` must render attached collection methods as a plain-language row subtitle on the owning row (`via platform API`, `via - Pulse Agent`, or `via platform API and Pulse Agent`), with fuller detail in +Pulse Agent`, or `via platform API and Pulse Agent`), with fuller detail in the edit dialog, instead of duplicating the same machine across multiple peer groups or forcing operators to decode badge jargon. The table-level product/system group rows in @@ -923,7 +1058,7 @@ not a replacement status card, CTA band, or page-local nested card. that destination. `InfrastructureSourceManager.tsx` exposes one Network discovery status/action band from the shared landing shell with scan state, saved scope, last result metadata, errors, `Run discovery`, `Discovery - settings`, and candidate review when discovered sources are waiting. It must +settings`, and candidate review when discovered sources are waiting. It must not start a network scan just because the page rendered. New-source admission belongs on the table's per-platform `Add` actions, the compact first-run/readiness actions, or the discovery band's explicit review action, @@ -1027,8 +1162,8 @@ not a replacement status card, CTA band, or page-local nested card. become the primary top-level system wording when a provider/API platform or reported host OS/appliance identity better explains what the operator is looking at. -6. Keep Proxmox deep-link route selection on the shared settings-navigation boundary. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` and `frontend-modern/src/components/Settings/useSettingsNavigation.ts` must treat the canonical PBS and PMG Proxmox deep links as agent-selection authority even though those URLs resolve to the shared `infrastructure-operations` tab. Reloading or remounting on a PBS or PMG deep link must not silently fall back to the PVE selector state. -7. Keep shared storage feature presenters on canonical platform truth. When reusable storage presenters under `frontend-modern/src/features/storageBackups/` classify canonical resources for the shared storage route, API-backed virtualization datastores such as VMware must stay inventory-only datastores instead of inheriting PBS-specific backup-repository or protected-target copy from older fallback branches. +5. Keep settings deep-link route selection on the shared settings-navigation boundary. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` and `frontend-modern/src/components/Settings/useSettingsNavigation.ts` must treat the canonical PBS and PMG Proxmox deep links as agent-selection authority even though those URLs resolve to the shared `infrastructure-operations` tab. Reloading or remounting on a PBS or PMG deep link must not silently fall back to the PVE selector state. Assistant OAuth callback compatibility queries such as `ai_oauth_error` and `ai_oauth_success` must route the bare settings root to Pulse Intelligence > Provider & Models while preserving the query long enough for `useAISettingsState` to consume and clear it, rather than normalizing the user back to Infrastructure and dropping the callback result. +6. Keep shared storage feature presenters on canonical platform truth. When reusable storage presenters under `frontend-modern/src/features/storageBackups/` classify canonical resources for the shared storage route, API-backed virtualization datastores such as VMware must stay inventory-only datastores instead of inheriting PBS-specific backup-repository or protected-target copy from older fallback branches. Those reusable storage presenters must also keep primary issue copy separate from contextual impact copy. Composite posture fields may include dependent resource or protected workload impact, but shared table/presenter primitives @@ -1083,7 +1218,7 @@ not a replacement status card, CTA band, or page-local nested card. Jobs, and CronJobs, including targets, active/current counts, ready/succeeded counts, availability, exceptions, service names, schedules, and last run metadata. -8. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, host-profile labels, surface-kind guesses, readiness-state guesses, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest. The generated governance/readiness split is authoritative: supported platform arrays drive current support claims, while admitted platform arrays may keep route/navigation and add-flow vocabulary available without turning `first-lab-ready` entries such as VMware into supported-source copy. Kubernetes manifest projections must enumerate the native API-backed page sections the shared tab shell can expose, including controllers, networking, storage, config, policy, autoscaling, and events, so platform pages do not invent local support claims outside the governed JSON. The generated `surface_kind` is the machine-readable boundary between owning platform entries and runtime lenses: `docker` is a `runtime-lens`, not a `platform`, even when the container-runtime route stays available as a primary shell destination. The generic `docker` source-platform label is "Docker / Podman" in shared selectors, badges, and filter options so v5 Docker users can find the runtime surface while Podman-backed rows are not mislabeled as Docker-only; "Container runtime" remains the governed runtime family, not the primary customer-facing label. Identity colour is semantic, not page-local decoration: shared source/platform badges, host identity badges, and container runtime badges must use the shared presentation helpers so Docker remains on the Docker/Podman blue runtime tone, Podman uses its distinct runtime tone, Proxmox PVE remains orange, and those meanings do not drift across table rows, filters, drawers, or platform pages. Agent host-profile entries, including Unraid, stay in the generated `agentHostProfiles` projection and shared wrapper helpers; frontend primitives may render those labels for Pulse Agent install/identity copy but must not add them to the first-class platform union. +7. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, host-profile labels, surface-kind guesses, readiness-state guesses, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest. The generated governance/readiness split is authoritative: supported platform arrays drive current support claims, while admitted platform arrays may keep route/navigation and add-flow vocabulary available without turning `first-lab-ready` entries such as VMware into supported-source copy. Kubernetes manifest projections must enumerate the native API-backed page sections the shared tab shell can expose, including controllers, networking, storage, config, policy, autoscaling, and events, so platform pages do not invent local support claims outside the governed JSON. The generated `surface_kind` is the machine-readable boundary between owning platform entries and runtime lenses: `docker` is a `runtime-lens`, not a `platform`, even when the container-runtime route stays available as a primary shell destination. The generic `docker` source-platform label is "Docker / Podman" in shared selectors, badges, and filter options so v5 Docker users can find the runtime surface while Podman-backed rows are not mislabeled as Docker-only; "Container runtime" remains the governed runtime family, not the primary customer-facing label. Identity colour is semantic, not page-local decoration: shared source/platform badges, host identity badges, and container runtime badges must use the shared presentation helpers so Docker remains on the Docker/Podman blue runtime tone, Podman uses its distinct runtime tone, Proxmox PVE remains orange, and those meanings do not drift across table rows, filters, drawers, or platform pages. Agent host-profile entries, including Unraid, stay in the generated `agentHostProfiles` projection and shared wrapper helpers; frontend primitives may render those labels for Pulse Agent install/identity copy but must not add them to the first-class platform union. The generated host-profile projection also carries runtime platform fallback metadata for shared explanation and parity with backend normalization, but frontend primitives must still render host-profile labels through @@ -1106,11 +1241,11 @@ not a replacement status card, CTA band, or page-local nested card. system badge names a platform with its version, source/method context may still add collection labels such as Pulse Agent, but it must not repeat the same platform again as an unversioned source badge. -10. Keep summary chart interaction identity on one shared helper. Summary surfaces that expose row-hover, group-hover, chart-hover, or route-focus-driven chart emphasis must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` and pass that same resolved scope into card-state, sparkline, and density-map primitives, rather than letting cards read `hovered || focused` while charts listen to a different page-local ID source. Hovering one summary chart must promote that series into the shared active entity so sibling cards highlight the same object instead of keeping chart-local hover islands, and hovering or pinning a workload group header, infrastructure cluster header, or storage pool-group header must scope the matching summary cards through that same shared contract instead of forking a page-local summary filter path. Sibling cards should surface that synchronized hover as one compact header readout through the shared summary-card contract, while the chart under the pointer keeps the only floating tooltip. Recovery is explicitly outside this interaction dialect: its retired posture-card strip must not return with row/group/chart hover behavior without a separate governed product decision. -11. Keep page summaries page-scoped when table rows enter contextual focus. Route-backed row selection may add a focused label and shared series emphasis, but infrastructure, workloads, and storage summary cards must continue to render the page-level series set instead of collapsing the summary down to the selected row or replacing the global trend view with row-local empty states. -12. Keep contextual row focus on the shared summary primitive. Summary surfaces and same-route table drill-ins must reuse `frontend-modern/src/components/shared/contextualFocus.ts` for interactive-series filtering, focused-name lookup, active-series derivation, local scroll preservation, and deliberate inline-detail reveal instead of rebuilding page-local `Set` filters, focused-label scans, drawer-aware scroll math, or ad hoc scroll restoration in each surface. -13. Keep summary-linked table row emphasis on the shared primitive contract. Workloads, infrastructure, and storage rows that mirror the active summary entity must expose that state through `data-summary-row-active` and let the shared presentation in `frontend-modern/src/index.css` render the row emphasis, rather than carrying page-local sky or blue fill classes inside each row renderer. Group-scoped preview and pin must use that same shared presentation boundary: child rows that belong to a hovered or pinned summary group should expose `data-summary-group-member-active="preview|pinned"` so the block-level emphasis stays subtle, consistent, and reversible instead of each table inventing its own outline, badge, or full-strength fill treatment. Static grouped row headers on workloads, infrastructure, storage, recovery, and future grouped tables must use `frontend-modern/src/components/shared/groupedTableRowPresentation.ts` plus the `.grouped-table-row` CSS contract in `frontend-modern/src/index.css`, rather than rebuilding local `bg-surface-alt` variants with subtly different light/dark behavior or page-local left-accent markers. That shared grouped-table primitive owns the subgroup cell padding, typography, small metadata, and badge treatment as well as the row background token, so a future adjustment to the subgroup visual language changes every grouped product table from one owner. Inline table detail rows on platform, workload, and infrastructure tables must compose `frontend-modern/src/components/shared/InlineDetailTableRow.tsx` for the full-width row, surface-alt cell, detail padding, and row-click containment instead of rebuilding page-local `TableRow` / `TableCell` / `div` shells around each drawer. Storage-backed reusable row presenters under `frontend-modern/src/features/storageBackups/` must also keep row height and alert accents on class/data-attribute presentation instead of runtime inline style maps, so the shared table contract stays CSP-safe on both steady-state and alert-highlighted routes. -14. Keep retained-value data loading honest at the ownership boundary. Helpers +8. Keep summary chart interaction identity on one shared helper. Summary surfaces that expose row-hover, group-hover, chart-hover, or route-focus-driven chart emphasis must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` and pass that same resolved scope into card-state, sparkline, and density-map primitives, rather than letting cards read `hovered || focused` while charts listen to a different page-local ID source. Hovering one summary chart must promote that series into the shared active entity so sibling cards highlight the same object instead of keeping chart-local hover islands, and hovering or pinning a workload group header, infrastructure cluster header, or storage pool-group header must scope the matching summary cards through that same shared contract instead of forking a page-local summary filter path. Sibling cards should surface that synchronized hover as one compact header readout through the shared summary-card contract, while the chart under the pointer keeps the only floating tooltip. Recovery is explicitly outside this interaction dialect: its retired posture-card strip must not return with row/group/chart hover behavior without a separate governed product decision. +9. Keep page summaries page-scoped when table rows enter contextual focus. Route-backed row selection may add a focused label and shared series emphasis, but infrastructure, workloads, and storage summary cards must continue to render the page-level series set instead of collapsing the summary down to the selected row or replacing the global trend view with row-local empty states. +10. Keep contextual row focus on the shared summary primitive. Summary surfaces and same-route table drill-ins must reuse `frontend-modern/src/components/shared/contextualFocus.ts` for interactive-series filtering, focused-name lookup, active-series derivation, local scroll preservation, and deliberate inline-detail reveal instead of rebuilding page-local `Set` filters, focused-label scans, drawer-aware scroll math, or ad hoc scroll restoration in each surface. +11. Keep summary-linked table row emphasis on the shared primitive contract. Workloads, infrastructure, and storage rows that mirror the active summary entity must expose that state through `data-summary-row-active` and let the shared presentation in `frontend-modern/src/index.css` render the row emphasis, rather than carrying page-local sky or blue fill classes inside each row renderer. Group-scoped preview and pin must use that same shared presentation boundary: child rows that belong to a hovered or pinned summary group should expose `data-summary-group-member-active="preview|pinned"` so the block-level emphasis stays subtle, consistent, and reversible instead of each table inventing its own outline, badge, or full-strength fill treatment. Static grouped row headers on workloads, infrastructure, storage, recovery, and future grouped tables must use `frontend-modern/src/components/shared/groupedTableRowPresentation.ts` plus the `.grouped-table-row` CSS contract in `frontend-modern/src/index.css`, rather than rebuilding local `bg-surface-alt` variants with subtly different light/dark behavior or page-local left-accent markers. That shared grouped-table primitive owns the subgroup cell padding, typography, small metadata, and badge treatment as well as the row background token, so a future adjustment to the subgroup visual language changes every grouped product table from one owner. Inline table detail rows on platform, workload, and infrastructure tables must compose `frontend-modern/src/components/shared/InlineDetailTableRow.tsx` for the full-width row, surface-alt cell, detail padding, and row-click containment instead of rebuilding page-local `TableRow` / `TableCell` / `div` shells around each drawer. Storage-backed reusable row presenters under `frontend-modern/src/features/storageBackups/` must also keep row height and alert accents on class/data-attribute presentation instead of runtime inline style maps, so the shared table contract stays CSP-safe on both steady-state and alert-highlighted routes. +12. Keep retained-value data loading honest at the ownership boundary. Helpers that prevent a feature surface from falling through the app-level Suspense boundary during in-flight refresh should stay feature-local until multiple governed surfaces truly share the behavior. Once that boundary is shared, @@ -1118,7 +1253,7 @@ not a replacement status card, CTA band, or page-local nested card. `frontend-modern/src/hooks/createNonSuspendingQuery.ts` rather than re-copying suspense-escape logic into each feature area or burying it inside one feature's private state model. -15. Keep shared commercial warning banners truthful about destination intent. +13. Keep shared commercial warning banners truthful about destination intent. When a shared banner renders both explanatory and commercial CTAs, those labels must resolve to distinct owned destinations or section anchors instead of presenting two different labels that land on the same @@ -1129,7 +1264,7 @@ not a replacement status card, CTA band, or page-local nested card. review destination for a current paid feature, it must scope the operator into the usage-owned policy ledger rather than plan-selection intent or CTA copy that frames the flow as monitored-system-cap expansion. -16. Keep assistant availability bootstrap on the shared app-shell boundary. +14. Keep assistant availability bootstrap on the shared app-shell boundary. `frontend-modern/src/useAppRuntimeState.ts`, `frontend-modern/src/App.tsx`, `frontend-modern/src/stores/aiChat.ts`, and @@ -1159,22 +1294,19 @@ not a replacement status card, CTA band, or page-local nested card. must stay on the app-shell assistant-availability fact instead of re-reading raw AI settings just to decide whether assistant affordances should render. -18. Keep Patrol shell composition and product-first provider vocabulary on the +15. Keep Patrol shell composition and product-first provider vocabulary on the shared feature-presentation boundary. - `frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx`, `frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx`, `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`, `frontend-modern/src/features/patrol/PatrolIntelligenceBanners.tsx`, `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`, `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`, - `frontend-modern/src/features/patrol/patrolSupportingContextPresentation.ts`, - `frontend-modern/src/components/patrol/PatrolStatusBar.tsx`, and + `frontend-modern/src/components/patrol/RunHistoryEntry.tsx`, and `frontend-modern/src/utils/patrolRuntimeActions.ts` must keep Patrol assessment, verification, and findings primary; surface recent - changes, learned correlations, and policy coverage only as explicitly - secondary supporting context when degraded or incomplete verification, - active findings, or selected-run investigation makes that evidence - relevant; and use Patrol/provider wording for the shared provider settings, + changes, learned correlations, and policy coverage only as backend, + Assistant, selected-finding, or selected-run context when investigation + makes that evidence relevant; and use Patrol/provider wording for the shared provider settings, provider model, and provider circuit-breaker affordances instead of generic AI labels inside Patrol-owned shells. The shared app shell in `frontend-modern/src/App.tsx` and `frontend-modern/src/AppLayout.tsx` must @@ -1183,33 +1315,65 @@ not a replacement status card, CTA band, or page-local nested card. second Patrol-branded primary route. `PatrolIntelligenceHeader.tsx` must also keep the page heading's accessible name singular: when the `PulsePatrolLogo` appears beside visible Patrol heading text, it is - decorative rather than a second label source. The Patrol-owned supporting-context - presenter must also keep the disclosure toggle plus evidence-boundary copy - centralized instead of letting the workspace reintroduce inline shell-local - trust wording, while the Patrol investigation-context owner normalizes - same-state recent-change records into changed-substate wording before the - workspace or Assistant handoff renders them. The same shared feature-shell + decorative rather than a second label source. The Patrol workspace must not + expose a generic Details/supporting-context panel for nearby activity, + learned correlations, or policy buckets; those payloads stay backend and + Assistant context rather than a default page section. Patrol initial data + refresh failures must stay inside the Patrol feature shell as one compact + stale-data retry banner; they must not replace the route with Suspense, + blank loading, raw transport errors, or page-local diagnostic panels. The Patrol + investigation-context owner normalizes same-state recent-change records into + changed-substate wording before Assistant handoff renders them. The same shared feature-shell boundary owns the - commercial-facing Patrol capability language: Pro-locked helper copy, - autonomy segmented controls, and run-history/result labels must use - `Remediate`, `remediated`, or safe remediation wording while legacy API - names remain hidden from operators. The Patrol configuration popover is part - of that shared feature-presentation boundary: it must stay viewport-bounded, - expose an accessible dialog label, and pass backend save rejection reasons - through as inline dialog state instead of replacing them with generic toast - copy. When the failure includes Patrol readiness context, the inline state - must expose the provider, model, and readiness summary next to a direct - provider-settings action instead of hiding that diagnosis behind Assistant - alone. The provider-model selector in that popover must stay bound to the - shared runtime settings/model catalog even when the popover mounts after - async catalog loading, so a saved direct-provider Patrol model renders as + commercial-facing Patrol capability language: autonomy segmented controls + and run-history/result labels must present the operator-facing policy levels + as `Watch only`, `Ask first`, `Safe auto-fix`, and `Autopilot`, while legacy + API names remain hidden from operators and compact controls do not collapse + into unexplained shorthand. Patrol run-history rows must lead with what + Patrol did or could not do before exposing trigger, token, tool-call, or raw + trace details. Plan-locked Patrol controls may badge paid levels as Pro and + may show one compact `Plans & Billing` handoff beside the selector when + upgrade prompts are allowed, but the default watch-only surface must not + render a Pro-absence explainer. Visible product copy calls the selector `Patrol mode`; compatibility route and wire identifiers may keep stable names + such as `patrol_control` and `patrolControl*`. Plan-locked Patrol controls + may keep paid levels visible as disabled buttons, and may badge them as Pro + only when upgrade prompts are allowed. The always-visible Patrol mode selector must stay on + the selected mode and one plain summary, without a separate `Limits` + disclosure or hard-limit matrix beside the picker. Shared feature shells must not invent their own Patrol safety + thresholds, policy labels, or disabled-control explanations. The Patrol page + header must consume the same effective control state, using watch-and-report + copy for locked or `Watch only` mode and full governed-operations copy only + for modes where that capability is actually available. Paid-control + availability and commercial-plan copy must describe the same decision as + choosing what Patrol may handle automatically; they must not ask users to + decide how far Patrol can go or how much control Patrol has. The + Current work workspace copy belongs to that same product-facing boundary: + empty and descriptive text must explain what Patrol-found problems will + appear there, what the selected control level allows, and the next useful + operator action; it must not fall back to activation-loop, proof, queue, or + verification-accounting language. The + Patrol schedule and model drawer is part of that shared + feature-presentation boundary: it must stay viewport-bounded, expose an + accessible dialog label, keep the four-level control policy on the default + Patrol header, and keep provider model, schedule, trigger tuning, and + readiness validation inside the secondary disclosure. Backend save rejection reasons must pass + through as inline dialog state instead of being replaced with generic toast + copy, and that advanced disclosure must open when the inline state exists. + When the failure includes + Patrol readiness context, the inline state must expose the provider, model, + and readiness summary next to a direct provider-settings action instead of + hiding that diagnosis behind Assistant alone. The provider-model selector in + that popover must stay bound to the shared runtime settings/model catalog + even when the popover mounts after async catalog loading, but the full + catalog must stay behind an explicit change action so the default advanced + drawer leads with the current effective model summary rather than a raw + provider route list. A saved direct-provider Patrol model still renders as that model instead of visually falling back to the default selection. Successful provider-model saves that return a not-ready Patrol readiness snapshot must use that same inline surface with `needs attention` wording, while Assistant receives a saved configuration issue rather than a - failed-save handoff. When safe remediation is locked, the same popover - state owner must - clear stale full-mode unlock state before Apply Configuration submits the + failed-save handoff. When governed fixes are locked, the same Patrol state + owner must clear stale full-mode unlock state before persisting the monitor-only autonomy payload, so disabled paid controls cannot leak stale permission into a save. If that inline state opens Assistant, the Patrol feature must hand off @@ -1222,14 +1386,35 @@ not a replacement status card, CTA band, or page-local nested card. findings tab, run-scoped findings panels, and tab badges so shared feature composition does not rebuild Patrol state by filtering the cross-product unified findings feed. -19. Keep the shared `system-ai` settings shell product-first. +16. Keep Pulse Intelligence settings product-first and page-scoped. `frontend-modern/src/components/Settings/AISettings.tsx`, `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`, `frontend-modern/src/components/Settings/settingsNavCatalog.ts`, + `frontend-modern/src/components/Settings/settingsNavigationModel.ts`, + `frontend-modern/src/components/Settings/settingsPanelRegistry.ts`, + `frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx`, `frontend-modern/src/components/Settings/useAISettingsState.ts`, and `frontend-modern/src/utils/aiSettingsPresentation.ts` must present that - surface to operators as `Assistant & Patrol` plus provider/model - configuration rather than as a generic `AI Services` shell. Settings-save + surface to operators under the `Pulse Intelligence` settings group as + separate focused pages rather than as a generic `AI Services` shell or one + oversized mixed form. `Provider & Models` owns API keys, default model + selection, provider health/preflight, provider runtime budget/timeout, and + usage/cost visibility. `Patrol` owns schedule, alert/anomaly triggers, + runtime readiness, Patrol model override, and a simple `Open Patrol` handoff + to the `/patrol` operator page; the actual watch/investigate/act/verify/record + operator loop stays on `/patrol`. `Assistant` owns chat/tool permission, + command-access, model override, and session maintenance. Service context may + exist under Pulse Intelligence only for model-backed or continuous service + discovery that supplies Assistant and Patrol context; normal infrastructure + discovery and onboarding remain under Infrastructure, and the Pulse + Intelligence navigation item, route header, model override, reset/save + affordances, and setup copy must use the `Service Context` label so + operators do not confuse it with infrastructure discovery. The + `Provider & Models` page must not carry a discovery summary, + Patrol-control banner, or Patrol CTA; the Patrol-control handoff belongs on + the `Patrol` settings page and `/patrol`, where copy describes Patrol + autonomy in plain operator terms rather than exposing an internal + `operations policy` concept. Settings-save feedback must preserve provider-specific preflight failures and successful save responses that carry Patrol readiness warnings, including the provider, selected Patrol model, failure cause, safe recommendation, and readiness @@ -1239,37 +1424,57 @@ not a replacement status card, CTA band, or page-local nested card. setup cards must describe provider families through the current backend-owned provider contract; DeepSeek setup copy is the V4 family and must not regress to old V3 or compatibility-alias wording. + Provider connection controls are page-scoped: the global Pulse + Intelligence enable toggle, provider readiness strip, and Test Connection + action belong to `Provider & Models`; Patrol, Assistant, and Service Context + subpages keep their focused settings plus reset/save actions without + repeating provider health chrome. Those reset/save actions and save + notifications must be page-scoped as well: saving `Patrol`, `Assistant`, + or `Service Context` settings must not report + `Provider & Models settings saved` or render a generic `Save changes` + affordance that hides which Pulse + Intelligence page owns the change. Runtime controls inside `frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx` - must likewise describe discovery as workload discovery that supplies - concrete service context to Pulse Assistant and Patrol, not as a generic - AI context feature. `frontend-modern/src/components/Settings/useAISettingsState.ts` - must save workload discovery enablement and interval as one explicit + must stay split by page-specific exports: provider runtime controls on + `Provider & Models`, Assistant chat actions on `Assistant`, and service + context controls on `Service Context`. Assistant chat-action copy + must name Patrol control as configured on the Patrol page, not as an + Assistant command mode, because `/patrol` remains the operator surface for + choosing how much autonomy Patrol has. Service context copy must describe + the model-backed loop that supplies concrete service facts to Pulse + Assistant and Patrol, not as generic discovery or AI context. + `frontend-modern/src/components/Settings/useAISettingsState.ts` + must save service context scan enablement and interval as one explicit settings pair so selecting "Every 6 hours" or "Manual only" round-trips through `/api/settings/ai` without depending on stale read-side diffing. - The same workload-discovery settings section must expose a manual - "Run discovery now" action wired through `/api/discovery/run` when - workload discovery is enabled in manual-only mode, while resource-drawer + The same Service Context settings section must expose a manual + context-scan action wired through `/api/discovery/run` when + service context scanning is enabled in manual-only mode, while resource-drawer discovery remains the forced single-resource refresh path. The collapsed section and run-action copy must make automatic scheduling visible by distinguishing `Auto `, `Manual only`, and `Off`, and the run - action must describe whether it is executing the scheduled sweep or a + action must describe whether it is running the scheduled scan or a one-off manual-only sweep rather than implying recurring scans were enabled. - Assistant-only controls inside the shared shell, such - as execution permissions and session maintenance, must stay explicitly - labeled as Pulse Assistant controls, while Patrol schedule and autonomy - continue to live on Patrol-owned surfaces rather than drifting back into - the shared settings shell. Session maintenance in that shell is limited to + Assistant-only controls such as execution permissions and session + maintenance must stay explicitly labeled as Pulse Assistant controls, while + Patrol schedule and trigger readiness live on the Patrol settings page and + Patrol autonomy/control level lives on the `/patrol` operator page rather + than drifting back into the provider shell. Session maintenance is limited to Pulse-owned conversation operations such as summarization; OpenCode-style file diff, revert, or unrevert actions must not appear in Settings unless Pulse owns a real governed infrastructure action-history/reversal contract - for the affected resources. Shared/default model choices may remain on the - combined shell only when Assistant, Patrol, and Discovery overrides are - presented as explicit per-surface overrides instead of a generic advanced - AI bucket. Each per-surface override must fall back to the shared default - when left empty rather than silently using a hard-coded backend default, - so the shared shell stays the single place an operator picks the model - for any of the three surfaces. + for the affected resources. Shared/default model choice belongs on + `Provider & Models`, while Assistant, Patrol, and service context model + overrides belong on their respective settings pages instead of a generic + advanced AI bucket. Each per-surface override must fall back to the shared + default when + left empty rather than silently using a hard-coded backend default, so + `Provider & Models` stays the single place an operator picks the default + model for all three surfaces. Assistant model copy must describe chat, + explanation, and review support; it must not present Assistant as the + approved-fix executor because Patrol is the hands-on operator for checks, + governed fixes, and verification. The shared shell must not show Pro-only autonomous execution as a default free-user control when upgrade prompts are suppressed; it may surface that option only when the entitlement is present, commercial prompts are @@ -1317,7 +1522,7 @@ not a replacement status card, CTA band, or page-local nested card. selected route also carries a shared-default or override badge, the shared picker owns that badge as separate metadata in both visible text and the button accessible name; labels must render as `model via OpenRouter · - default` instead of fusing provider and badge text such as +default` instead of fusing provider and badge text such as `OpenRouterdefault`. Platform-first top-level pages registered through `frontend-modern/src/App.tsx` must stay chrome-only and route through the @@ -1376,6 +1581,47 @@ not a replacement status card, CTA band, or page-local nested card. first/default positions only when the current estate has standalone Pulse Agent machines or agentless availability endpoints and no provider/runtime platform evidence. + Patrol workflow components under + `frontend-modern/src/features/patrol/` may compose shared `Button` and + `ButtonLink` chrome for issue actions, but the workflow state, route + anchors, single-finding direct-action selection, Assistant handoff, autonomy + label, and multi-finding fallback semantics stay with `patrol-intelligence`; + the canonical Patrol control anchor belongs on the visible selector, not the + workspace shell. Setup-only readiness may hide run, schedule, model, trigger, + and provider-repair controls, but it must not hide that selector or replace + it with a setup/status explainer. Shared primitives must not grow + Patrol-specific activation, autonomy, provider-settings, or + Assistant-routing behavior. The default loop is a + Patrol-owned watch / investigate / act under policy / verify / record loop. + Active current-issue expansion is a task surface, not a history transcript: + raw finding lifecycle rows may render in explicit all/resolved/history or + selected-run review states, but not in the default active Patrol issue + expansion. + Compact Patrol status chrome may render work/health evidence passed by + Patrol, but trigger/scheduling status remains header/control context and + must not be repeated by shared default status primitives. Shared primitives + must preserve that plain visible label instead of exposing internal + assessment terminology on the default page. + External-agent readiness from Pulse MCP may remain compact optional context + derived from the shared manifest-client contract verdict and backend + operations-loop `externalAgentReady` signal, but shared primitives must not + create page-local MCP setup constants, token-scope checks, tool filters, + readiness shortcuts, MCP readiness props, or a visible external-agent stage + as the primary first-party loop. The journey's loaded progress state must come from the + canonical operations-loop status projection exposed by the shared + agent-capabilities frontend client, while shared primitives remain passive + renderers of the state Patrol passes them. Patrol control starter, + completed-loop, or resolved-loop evidence may change compact journey copy + only after Patrol has derived it from that projection; shared primitives must + not infer Patrol control or legacy Pro activation state from route anchors, + billing state, generic Patrol recency, or MCP readiness alone. + Shared presentation + helpers may render the operations-loop state they receive, but they must + not infer operations-loop progress from a generic Patrol run, recency + timestamp, or MCP readiness alone; Patrol owns the issue-backed evidence + model that decides when the loop can advance through Assistant, approval, + rejected no-execution terminal decisions, approved-action verification, and + external-agent parity. ## Forbidden Paths @@ -1390,7 +1636,12 @@ not a replacement status card, CTA band, or page-local nested card. conversion, or infrastructure onboarding telemetry. Those signals belong in admin-owned metrics surfaces, not the product diagnostics UI or customer frontend event emission. -6. Settings route models normalizing retired aliases such as +6. User-facing diagnostics panels rendering the native Pulse Assistant runtime + as an MCP connection. Settings diagnostics must consume + `assistantRuntimeConnected` and label it as Assistant runtime availability; + `mcpConnected`, `mcpToolCount`, and "MCP Connection" are forbidden on the + first-party diagnostics surface. +7. Settings route models normalizing retired aliases such as `/settings/operations/*`, `/settings/integrations/api`, `/settings/system-pro`, `/settings/workloads/*`, or nested `/settings/infrastructure/*` paths back into current settings panels. @@ -1424,28 +1675,32 @@ not a replacement status card, CTA band, or page-local nested card. placeholders must stay on `LoadingSpinner` and the `loading-spinner-shell` registry rule instead of local `border-t-transparent` or `border-b-2` animate-spin shells. -3. Update this contract when a new canonical UI pattern is adopted -4. Remove local forks after the shared primitive is introduced -5. Keep shared feature-level presenters on capability truth. When reusable +4. Update this contract when a new canonical UI pattern is adopted +5. Remove local forks after the shared primitive is introduced +6. Keep shared feature-level presenters on capability truth. When reusable presenters under `frontend-modern/src/features/` explain why a control, chart, or detail surface is unavailable, they must describe the owned identity or capability gap instead of prescribing a provider-local install path that conflicts with API-backed platforms like TrueNAS. -6. When a settings route header and a top-level settings shell describe the same +7. When a settings route header and a top-level settings shell describe the same commercial surface, keep them on the same shared presentation owner instead of allowing route metadata in `settingsHeaderMeta.ts` or labels in `settingsNavCatalog.ts` to drift into independent title or description copy, and keep adjacent settings-shell referrals such as `InfrastructureWorkspace.tsx` on that same shared owner instead of reintroducing local “go to Pulse Pro” variants. - That same shared owner must also support explicit IA/title separation for - self-hosted commercial settings: the nav label may stay product-IA-first - (`Plans`) while the page shell stays task-first (`Self-hosted plan`), and - the owned plan shell must foreground the active plan name plus available + That same shared owner must also keep self-hosted commercial settings + discoverable: the nav label and page shell title use the shared + `Plans & Billing` label, and the owned plan shell must foreground the active + plan name plus available capabilities before secondary billing or recovery detail so paid upgrades can confirm their entitlement immediately after activation without making default Community look like it is missing an activation key. -7. When settings surfaces need informational, warning, success, or danger + Routine plan and capability-status copy must stay product-facing: describe + what is available on the instance, point failed capability checks to refresh + the plan or open recovery, and avoid raw entitlement-payload phrasing or + activation language as the normal setup story. +8. When settings surfaces need informational, warning, success, or danger callouts, compose `frontend-modern/src/components/shared/CalloutCard.tsx` and register the consumer in `frontend-modern/scripts/shared-template-registry.json` instead of adding @@ -1462,7 +1717,7 @@ not a replacement status card, CTA band, or page-local nested card. boundary. The update flow owns version, prerequisite, root-access, restart, and error copy; `CalloutCard` owns the info/warning/danger shell, spacing, dark-mode tone, and icon layout. -8. Keep hosted settings-shell framing imports safe for bundle initialization. +9. Keep hosted settings-shell framing imports safe for bundle initialization. Self-hosted billing titles, descriptions, and referral copy used by `settingsHeaderMeta.ts`, `settingsNavCatalog.ts`, and adjacent settings shells must flow through @@ -1474,28 +1729,32 @@ not a replacement status card, CTA band, or page-local nested card. badge titles, Pro-suffixed option labels, monitored-system limit claims, or browser-local commercial/onboarding metrics wrappers in SSO, audit, reporting, AI controls, agent profiles, or shared warning banners. -9. Keep shared settings-shell AI control copy capability-scoped rather than - upsell-scoped. `AIRuntimeControlsSection.tsx` may describe read-only, - approval-required, and autonomous action posture, but option labels and - helper text must avoid tier labels or broad "executes everything" wording; - paid capability availability belongs to entitlement-backed visibility and - lock state, not local select copy. -10. Keep first-session dashboard empty-state copy on - `frontend-modern/src/utils/workloadEmptyStatePresentation.ts`, and make - infrastructure setup guidance name the canonical destination explicitly - instead of falling back to generic settings CTA labels. -9. Keep the live first-session wizard on the canonical three-step runtime - shape in `frontend-modern/src/components/SetupWizard/SetupWizard.tsx` - (`Welcome`, `Security`, then `Install`), and keep the step indicator plus - completion CTA language aligned with the governed infrastructure install - workspace instead of regressing to a route jump that leaves the next action - implicit. Preview-only follow-up surfaces such as - `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx` - must stay deterministic and scenario-driven: they may not poll the live - `/api/state` runtime or inherit whatever connected systems happen to exist - on the current backend, and browser proof for `/preview/setup-complete` - must select explicit preview scenarios instead of ambient runtime state. -10. Keep AI settings setup UI backend-driven: +10. Keep shared settings-shell AI control copy capability-scoped rather than + upsell-scoped. `AIRuntimeControlsSection.tsx` may describe read-only, + approval-required, and autonomous action posture, but option labels and + helper text must avoid tier labels or broad "executes everything" wording; + paid capability availability belongs to entitlement-backed visibility and + lock state, not local select copy. Provider & Models settings copy must keep + Patrol autonomy distinct from Assistant chat actions: Patrol's + hands-on control level belongs on the Patrol page, while the shared settings + shell may only describe whether Assistant chat can run eligible chat + actions. +11. Keep first-session dashboard empty-state copy on + `frontend-modern/src/utils/workloadEmptyStatePresentation.ts`, and make + infrastructure setup guidance name the canonical destination explicitly + instead of falling back to generic settings CTA labels. +12. Keep the live first-session wizard on the canonical three-step runtime + shape in `frontend-modern/src/components/SetupWizard/SetupWizard.tsx` + (`Welcome`, `Security`, then `Install`), and keep the step indicator plus + completion CTA language aligned with the governed infrastructure install + workspace instead of regressing to a route jump that leaves the next action + implicit. Preview-only follow-up surfaces such as + `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx` + must stay deterministic and scenario-driven: they may not poll the live + `/api/state` runtime or inherit whatever connected systems happen to exist + on the current backend, and browser proof for `/preview/setup-complete` + must select explicit preview scenarios instead of ambient runtime state. +13. Keep AI settings setup UI backend-driven: `frontend-modern/src/components/Settings/useAISettingsState.ts` and `frontend-modern/src/components/Settings/AISettingsDialogs.tsx` may collect provider credentials or runtime URLs, but they must not bake vendor model @@ -1608,19 +1867,19 @@ not a replacement status card, CTA band, or page-local nested card. or proposed fix, must pass `autonomousMode:false` as a request-local override so the drawer shows approval-required posture without mutating the persistent Assistant control setting. -11. Keep shared filter primitives coherent with source-owned option hydration. +14. Keep shared filter primitives coherent with source-owned option hydration. Active platform/runtime pages and Settings infrastructure surfaces must keep canonical options visible in shared filter controls even when current results do not contain that option, so provider- or endpoint-scoped handoffs do not flash back to generic host-only language. -12. Keep the first welcome screen in +15. Keep the first welcome screen in `frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx` explicit about operator context. The shell must explain that the bootstrap token only unlocks first-run setup, state where the command should run, and adapt command/help text to detected Docker or containerized deployments instead of assuming the operator already knows which host or container owns the Pulse install. -13. Keep the settings-shell infrastructure landing path aligned with that same +16. Keep the settings-shell infrastructure landing path aligned with that same first-session story. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` must treat `/settings` and the infrastructure settings tab as the canonical path to the bare `/settings/infrastructure`, which renders the unified @@ -1629,7 +1888,7 @@ not a replacement status card, CTA band, or page-local nested card. and the `Add infrastructure` entry point on it, not by a second landing route, so first-time operators and returning operators see one consistent infrastructure surface by default. -14. Keep Infrastructure and Workloads onboarding copy on the shared +17. Keep Infrastructure and Workloads onboarding copy on the shared presentation owner in `frontend-modern/src/utils/workloadEmptyStatePresentation.ts`. Both the infrastructure empty state and the Workloads no-resources state must route @@ -1643,28 +1902,37 @@ not a replacement status card, CTA band, or page-local nested card. operators at credentials, permissions, and collection status in the canonical infrastructure workspace instead of reusing first-run onboarding copy. -15. Keep cross-surface investigation handoffs on shared route ownership. +18. Keep cross-surface investigation handoffs on shared route ownership. Feature shells such as Alerts and Patrol may decide which governed destination chips to render, but canonical href, label, dedupe, and infrastructure-fallback truth must stay in `frontend-modern/src/routing/resourceLinks.ts` instead of freezing raw route strings or provider-local link builders inside feature panels. -16. Keep shared summary-card emphasis coherent. When shared summary primitives enter an `inactive` state, `SummaryMetricCard`, `InteractiveSparkline`, and `DensityMap` must all demote background context together so storage, infrastructure, and workloads read as one interaction model instead of mixing page-local opacity, sticky-shell, or highlight rules. -17. Keep density-map summaries overview-first. When a shared summary density map receives row focus or chart-hover emphasis, `frontend-modern/src/components/shared/DensityMap.tsx`, `frontend-modern/src/components/shared/useDensityMapState.ts`, and `frontend-modern/src/components/shared/densityMapModel.ts` must preserve the multi-entity overview rows and keep focused-entity detail in the hover tooltip instead of swapping the card into a single-series chart, dimming the rest of the map into unusable background noise, duplicating cursor-value tooltip copy, or adding persistent card chrome that steals heatmap space. The card body must stay overview-first; the tooltip may carry the active entity identity, current value, and peak, shared tooltip shells must follow semantic surface tokens instead of forcing a dark palette in light mode, the tooltip header must let long entity names consume the available width before truncating rather than clipping against an arbitrary fixed label cap, numeric metric readouts such as `16.9 MB/s` or `37.4 MB/s` must stay single-line instead of wrapping the unit onto a second row, and density-map detail that cannot fit cleanly inside the canonical tooltip shell must be omitted rather than introducing tooltip-specific chrome or a secondary chart inside the hover surface. -18. Keep retired self-hosted hosted-model and trial acquisition surfaces out of + Patrol workflow handoffs follow the same rule: start/continue Patrol + control links must compose the route-backed `patrol_control` helper from + `resourceLinks.ts`, single-finding direct action links must use canonical + finding-presentation destinations such as the Patrol provider-settings + route, while `patrol_autonomy` and legacy Pro activation URLs remain parser + aliases only and verified review links use the plain Patrol history anchor. + UI surfaces must not duplicate the `patrolControlStarter` query string or + write Patrol control or legacy entry-point starter telemetry from local + click handlers. +19. Keep shared summary-card emphasis coherent. When shared summary primitives enter an `inactive` state, `SummaryMetricCard`, `InteractiveSparkline`, and `DensityMap` must all demote background context together so storage, infrastructure, and workloads read as one interaction model instead of mixing page-local opacity, sticky-shell, or highlight rules. +20. Keep density-map summaries overview-first. When a shared summary density map receives row focus or chart-hover emphasis, `frontend-modern/src/components/shared/DensityMap.tsx`, `frontend-modern/src/components/shared/useDensityMapState.ts`, and `frontend-modern/src/components/shared/densityMapModel.ts` must preserve the multi-entity overview rows and keep focused-entity detail in the hover tooltip instead of swapping the card into a single-series chart, dimming the rest of the map into unusable background noise, duplicating cursor-value tooltip copy, or adding persistent card chrome that steals heatmap space. The card body must stay overview-first; the tooltip may carry the active entity identity, current value, and peak, shared tooltip shells must follow semantic surface tokens instead of forcing a dark palette in light mode, the tooltip header must let long entity names consume the available width before truncating rather than clipping against an arbitrary fixed label cap, numeric metric readouts such as `16.9 MB/s` or `37.4 MB/s` must stay single-line instead of wrapping the unit onto a second row, and density-map detail that cannot fit cleanly inside the canonical tooltip shell must be omitted rather than introducing tooltip-specific chrome or a secondary chart inside the hover surface. +21. Keep retired self-hosted hosted-model and trial acquisition surfaces out of normal v6 GA runtime. Shared shells and helper-driven badges may continue to parse legacy payload fields, but ordinary self-hosted Assistant, Patrol, and settings flows must present provider setup as BYOK/local/self-managed and must not surface hosted-model credits, in-app trial starts, or generic managed-model claims. -19. Keep sparkline scrubbing source-local and sibling-sync timestamp-based. The chart a user is actively scrubbing in `frontend-modern/src/components/shared/InteractiveSparkline.tsx` and `frontend-modern/src/components/shared/useInteractiveSparklineState.ts` must keep its dashed hover cursor on the real local mouse `x`, while sibling cards may map the shared hover timestamp onto their own timelines. Shared cursor sync must not snap the source chart back onto the nearest sample timestamp, the rendered SVG/canvas hover cursor must bind to the actual numeric cursor coordinate rather than a boolean guard state, the time cursor must span the chart viewport instead of collapsing to the series height, and the hover tooltip must track the pointer instead of anchoring to the chart top edge while following the active theme rather than a hardcoded dark shell. The hover tooltip must stay side-offset from the active scrub cursor and flip to the available side near viewport edges so it does not cover the highlighted guide or graph point. -20. Keep shared contextual focus canonical after adoption. Once a summary or table surface enters route-backed contextual focus, future additions must extend `frontend-modern/src/components/shared/contextualFocus.ts` and its guardrail tests rather than forking another helper for workload IDs, resource IDs, or scroll-preserving same-route selection. -21. Keep shared infrastructure/resource selectors on the canonical agent-facet +22. Keep sparkline scrubbing source-local and sibling-sync timestamp-based. The chart a user is actively scrubbing in `frontend-modern/src/components/shared/InteractiveSparkline.tsx` and `frontend-modern/src/components/shared/useInteractiveSparklineState.ts` must keep its dashed hover cursor on the real local mouse `x`, while sibling cards may map the shared hover timestamp onto their own timelines. Shared cursor sync must not snap the source chart back onto the nearest sample timestamp, the rendered SVG/canvas hover cursor must bind to the actual numeric cursor coordinate rather than a boolean guard state, the time cursor must span the chart viewport instead of collapsing to the series height, and the hover tooltip must track the pointer instead of anchoring to the chart top edge while following the active theme rather than a hardcoded dark shell. The hover tooltip must stay side-offset from the active scrub cursor and flip to the available side near viewport edges so it does not cover the highlighted guide or graph point. +23. Keep shared contextual focus canonical after adoption. Once a summary or table surface enters route-backed contextual focus, future additions must extend `frontend-modern/src/components/shared/contextualFocus.ts` and its guardrail tests rather than forking another helper for workload IDs, resource IDs, or scroll-preserving same-route selection. +24. Keep shared infrastructure/resource selectors on the canonical agent-facet truth. Shared primitives and settings-facing selector helpers must treat top-level TrueNAS appliances as agent-facet infrastructure via shared helper ownership instead of reviving a direct `resource.type === 'truenas'` branch inside page shells, selectors, or reporting-resource type helpers. -22. Keep shared feature-shell Patrol run fixtures on the canonical run-record +25. Keep shared feature-shell Patrol run fixtures on the canonical run-record contract. When `frontend-modern/src/features/patrol/` consumes Patrol run history, the shared normalized record must preserve provider-backed counts such as `truenas_checked` instead of letting feature-local fixtures or @@ -1684,7 +1952,7 @@ not a replacement status card, CTA band, or page-local nested card. dashboard route is retired, that audit must also discover live top-level pages from `src/pages/` and may not keep a hard required-header entry for `frontend-modern/src/pages/Dashboard.tsx`. -23. Keep the authenticated app root aligned with that same first-session path. +26. Keep the authenticated app root aligned with that same first-session path. That same shared-primitive ownership now includes contextual row focus. `frontend-modern/src/components/shared/contextualFocus.ts` is the canonical owner for interactive-series filtering, focused-label lookup, active-series @@ -1724,35 +1992,35 @@ not a replacement status card, CTA band, or page-local nested card. renders the chart. `frontend-modern/src/useAppRuntimeState.ts` must not prewarm retired Infrastructure summary-chart caches or eager Workloads chart caches as a generic authenticated-shell side effect. -24. Keep relay settings shell copy on the shared presentation owner in +27. Keep relay settings shell copy on the shared presentation owner in `frontend-modern/src/utils/relayPresentation.ts`. The route metadata in `settingsHeaderMeta.ts` and the leading `SettingsPanel` in `RelaySettingsPanel.tsx` must reuse the same description and availability copy instead of drifting into separate rollout or pairing wording. Relay availability copy must describe the Relay tier boundary as Relay and higher plans rather than collapsing Remote Access back into a Pro-only feature. -25. Keep shared settings-shell legal and docs referrals on +28. Keep shared settings-shell legal and docs referrals on `frontend-modern/src/utils/docsLinks.ts`. Shared settings surfaces such as `AIRuntimeControlsSection.tsx` must not hardcode GitHub `main` doc URLs for privacy, security, proxy-auth, scope-reference, or Terms-of-Service links. -26. Keep shared settings-shell telemetry transparency controls on the governed +29. Keep shared settings-shell telemetry transparency controls on the governed general settings panel. Preview/reset affordances for anonymous telemetry must stay rendered inside `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` instead of drifting into route-local modals, hidden dev tools, or shell chrome that operators would not naturally inspect. -27. Keep the short telemetry/privacy summary copy on that same shared surface +30. Keep the short telemetry/privacy summary copy on that same shared surface accurate to the governed privacy doc. If the trust boundary depends on a specific retention window or on “IP addresses are not stored” rather than “IPs are never seen,” the summary copy in `GeneralSettingsPanel.tsx` must state those facts plainly instead of reverting to a stronger but inaccurate shorthand. -28. Keep maintainer commercial-event controls out of customer settings. +31. Keep maintainer commercial-event controls out of customer settings. The shared general settings privacy panel may expose anonymous outbound telemetry controls, preview, and reset affordances, but it must not render local commercial handoff event toggles, `PULSE_DISABLE_LOCAL_UPGRADE_METRICS`, or other commercial-debug controls as normal customer-facing preferences. -29. Keep shared storage-route feature presentation on neutral capability truth. +32. Keep shared storage-route feature presentation on neutral capability truth. Reusable mappers and presenters in `frontend-modern/src/features/storageBackups/` must distinguish inventory datastores from backup repositories so VMware rows on the shared storage route stay canonical to the admitted phase-1 floor instead of @@ -1774,7 +2042,7 @@ not a replacement status card, CTA band, or page-local nested card. must only carry fields the row actually renders; per-row source-platform badges and other identical-on-every-row decorations belong in the row expansion, not in `StoragePoolRowModel`. -30. Keep infrastructure settings-shell API alternatives on the shared shell +33. Keep infrastructure settings-shell API alternatives on the shared shell contract. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`, and `frontend-modern/src/components/Settings/settingsNavigationModel.ts` must @@ -1786,7 +2054,7 @@ not a replacement status card, CTA band, or page-local nested card. and no per-type `ProxmoxSettingsPanel` / `TrueNASSettingsPanel` / `VMwareSettingsPanel` to route through; the provider is a field inside one `ConnectionEditor`, not a destination. -31. Keep the infrastructure settings connection inventory on one shared +34. Keep the infrastructure settings connection inventory on one shared source. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx` composes rows exclusively from `frontend-modern/src/components/Settings/useConnectionsLedger.ts`, @@ -1796,14 +2064,14 @@ not a replacement status card, CTA band, or page-local nested card. `PlatformConnectionsWorkspace` / `TrueNASSettingsPanel` / `VMwareSettingsPanel` panels must not be reintroduced as a second fetch path. -32. Keep alert-history feature composition on the current owned state contract. +35. Keep alert-history feature composition on the current owned state contract. `frontend-modern/src/features/alerts/tabs/HistoryTab.tsx` must react to the shared `alertData()` history state instead of reviving deleted aliases, and it must pass unified-resource resolution through to `frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx` so the panel can render shared route chips without creating another page-local resource lookup or provider-specific handoff layer. -33. Keep the alert-thresholds containers surface on the canonical shared owner. +36. Keep the alert-thresholds containers surface on the canonical shared owner. `alertOverridesModel.ts`, `useAlertOverridesState.ts`, and `useAlertsConfigurationState.ts` must surface API-backed `app-container` parents such as TrueNAS as first-class `Container Runtimes`, while @@ -1812,7 +2080,7 @@ not a replacement status card, CTA band, or page-local nested card. props that can collapse functions on the live Solid surface. Docker-only controls in `ThresholdsTableDockerTab.tsx` must remain gated to real `docker-host` resources instead of leaking onto platform-managed runtimes. -34. Keep shared commercial upgrade navigation typed and destination-aware. +37. Keep shared commercial upgrade navigation typed and destination-aware. Shared paywall shells and upgrade actions must route internal billing or cloud destinations through `frontend-modern/src/utils/upgradeNavigation.ts`, `frontend-modern/src/components/shared/UpgradeLink.tsx`, and @@ -1823,24 +2091,25 @@ not a replacement status card, CTA band, or page-local nested card. width, tone, focus, route/new-tab behavior, and opener preservation stay on the shared `ButtonLink` primitive instead of page-local Tailwind anchors or commercial helper class strings. -35. Keep same-shell platform/runtime route transitions on retained shared state. +38. Keep same-shell platform/runtime route transitions on retained shared state. Active infrastructure consumers may show full-page loading only before the first compatible resource snapshot exists; once a fresh canonical snapshot is already present in the shared app shell, top-level platform/runtime tab switches must reuse that state boundary instead of flashing a transient page takeover between tabs. -36. Keep self-hosted paid-service prompts opt-in at the shared shell layer. +39. Keep self-hosted paid-service prompts opt-in at the shared shell layer. `settingsNavCatalog.ts`, `settingsNavVisibility.ts`, shared upgrade link primitives, trial banners, monitored-system warning banners, history-lock overlays, and Patrol lock helpers must honor `presentationPolicy.hideUpgrade` by hiding paid prompts by default on ordinary self-hosted installs. Direct activation/recovery routes may render their owned content, but sidebar - discovery, trial CTAs, plan upsells, monitored-system limit pressure, and - feature upgrade links must require hosted mode, explicit handoff, or active - entitlement. Cloud interest links from self-hosted plan surfaces must hand + discovery, trial CTAs, plan upsells, monitored-system limit pressure, + feature upgrade links, and plan-lock Patrol banners must require hosted + mode, explicit handoff, or active entitlement. Cloud interest links from + self-hosted plan surfaces must hand off to Pulse Account/public Cloud ownership rather than route to an in-product Cloud trial/signup page. -37. Keep the identified-service reducer on `discoveryPresentation.ts`. Any +40. Keep the identified-service reducer on `discoveryPresentation.ts`. Any surface that wants to label a workload with the AI-identified service (drawer overview card, future row chips, MCP capability payloads) must consume `getDiscoveryIdentifiedSummary` rather than re-implement the @@ -1870,6 +2139,33 @@ not a replacement status card, CTA band, or page-local nested card. ## Current State +Patrol `fix_rejected` presentation is owned by the Patrol/AI finding surfaces +that render the governed-action loop, while the surrounding badge, button, +loading, and icon composition still uses shared frontend primitives. Shared +primitive code must not special-case rejected Patrol fixes outside the standard +metadata badge, button, and lucide-icon contracts. +Setup-only Patrol action chrome is also governed by the shared Patrol workspace +composition contract: provider-blocked states may show only the setup task and +direct `Open Provider & Models` action, must suppress the duplicate readiness +banner, and must not expose run-history buttons as a competing primary action +before Patrol can check infrastructure. +Patrol page setup banners must stay at operator level: render Patrol readiness +payloads as `Patrol setup issue` or `Patrol setup warning`, keep provider/model +context visible, and keep preflight/tool-call diagnostic wording inside +Provider & Models rather than the first-party Patrol header. +Pulse Intelligence settings now keep `Provider & Models` focused on provider +setup, default model selection, health, budget, usage, and provider checks; it +must not reintroduce the Patrol-control banner or `Open Patrol control` CTA +that belongs to the `Patrol` settings page and `/patrol` operator surface. +The Patrol settings page may still hydrate the cached Patrol diagnostic +snapshot, but the rendered model-check panel must summarize it as model +readiness rather than exposing preflight/tool-call implementation wording. +The canonical Provider & Models browser route is +`/settings/pulse-intelligence/provider`; `/settings/system-ai` remains a +routeable compatibility alias for old deep links, while new settings +navigation, OAuth callback redirects, Assistant repair actions, and Patrol +provider-repair CTAs must emit the Pulse Intelligence route. + Shared loading indicators are part of the active frontend primitive contract. `LoadingSpinner` owns pure loading and action-pending spinner shells for shared primitive internals such as `Button`, `PulseDataGrid`, and @@ -1907,9 +2203,11 @@ The Patrol alert-trigger severity selector under `frontend-modern/src/features/patrol/` is built on the shared `FormSelect` primitive (label-for/id wiring, `selectBaseClass` styling hook) rather than a hand-rolled `