Files
pulse/docs/AGENT_SUBSTRATE.md
T
rcourtman 728c42e47b Bring action endpoints onto the agent surface with the agent-stable envelope
Closes the last known gap in the agent substrate. The three
action endpoints (POST /api/actions/plan, /api/actions/{id}/decision,
/api/actions/{id}/execute) previously emitted the platform-wide
APIError shape (stable code under "code", human under "error").
The agent surface uses the inverted shape (stable code under
"error", human under "message"), so adding action capabilities
to the manifest as-is would have forced agents to remember which
envelope each capability uses.

The slice refactors actions.go to emit the agent-stable envelope
across all 42 writeErrorResponse call sites. writeJSONError gains
a writeJSONErrorWithDetails sibling so the 13 calls that pass
field-level reasons (validation failures) preserve that
information under a new optional `details` field. The action
endpoints' JSON shape becomes:

  {"error": "<stable_code>", "message": "<human>",
   "details"?: {"<field>": "<reason>"}}

Frontend impact: zero. Verified that no frontend code consumes
the three action endpoints; the refactor is API-only.

Three new manifest entries (plan_action, decide_action,
execute_action) under a new "action" category, with their
declared error codes pinned per capability. Internal-failure 5xx
codes (audit-store outages, encode failures) are not declared
per capability; agents branch on 5xx generically.
TestContract_AgentSurfaceErrorCodesMatchManifestDeclarations now
audits actions.go alongside the existing two handler files, with
a documented internal-only allowlist for the 5xx codes.

The TestAgentSubstrate_ActionEndpointsEmitAgentStableEnvelope e2e
test exercises one error path through each endpoint via the actual
HTTP boundary, asserting the agent-stable envelope reaches the
wire and the legacy APIError fields (code, status_code, timestamp)
do NOT — drift back would mean the refactor regressed.

The TestContract_ActionDryRunOnlyExecutionErrorJSONSnapshot pin
is updated to match the new envelope shape; the manifest's
category allowlist gains "action".

api-contracts.md documents the new envelope (with details map),
the action governance loop's place in the substrate, the
ai:execute scope distinction from monitoring:write, and the
"manifest projection has a footnote" trade-off: bringing an
existing endpoint into the agent surface may require migrating
its error envelope, but the substrate keeps a single envelope
contract rather than carrying a translation wrapper layer.
agent-lifecycle.md and storage-recovery.md document the action
surface joining the agent paradigm and its zero-new-persistence
posture respectively. AGENT_SUBSTRATE.md's "what it does not do
yet" no longer lists the action surface; it now reflects the
real outstanding items (consumer feedback, an in-Pulse agent
integrations panel, a distribution path for pulse-mcp).
2026-05-10 15:16:17 +01:00

141 lines
6.5 KiB
Markdown

# Pulse agent substrate
A short, plain-English summary of what landed across the agent-paradigm
arc on `pulse/v6-release`. Suitable as the basis for release notes, a
GitHub announcement, or just a reminder to yourself in three weeks of
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
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`
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.
**Depth.** `/api/agent/resource-context/{id}` returns the situated
picture of one resource in a single read: identity, operator-set
state, active findings, pending approvals, recent actions including
refused dispatches and verification probe outcomes. Stable token
prefixes (`plan_drift:`, `resource_remediation_locked:`) reach the
wire verbatim so agents branch on codes, not human text.
**Breadth.** `/api/agent/fleet-context` returns a thin per-resource
rollup across the whole org: identity, operator flags, per-severity
finding counts, pending-approval count. One read for "where do I
focus?", with the per-resource bundle for follow-up depth.
**Write.** Two write surfaces. The operator-state intent loop
(`/api/resources/{id}/operator-state`) lets an agent record
per-resource commitments (intentionally offline, never
auto-remediate, maintenance window, criticality). The action
governance loop (`/api/actions/plan`, `/api/actions/{id}/decision`,
`/api/actions/{id}/execute`) lets an agent plan, approve, and
execute capability invocations against a resource through the
canonical audit store. The server populates attribution so client
values cannot spoof who-did-it. Validation failures emit the
`operator_state_invalid` and `invalid_action_request` stable
codes; lifecycle conflicts on the action loop emit
`action_not_pending`, `action_not_approved`,
`action_already_executing`, `action_execution_final`, and
`action_dry_run_only` so agents branch on the conflict rather
than retrying blindly.
**Push.** `/api/agent/events` is an SSE stream that fires
`finding.created`, `approval.pending`, and `action.completed` events
as state changes. Each event is a small fixed-shape payload with
enough context for an agent to decide whether to follow up. Refused
dispatches preserve their stable error tokens; successful dispatches
carry a verification block so agents close the certainty loop without
polling the audit endpoint.
## What ships consuming it
Two reference consumers, both standard-library-only, both
manifest-driven:
- **`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`,
`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.
## 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.
- 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."
## Provable claims
- **Manifest is honest.** A contract pin
(`TestContract_AgentSurfaceErrorCodesMatchManifestDeclarations`)
parses every `writeJSONError` call from agent-surface handlers and
every `ErrorCodes` declaration from the manifest, asserting
symmetry both directions. Drift either way fails the test.
- **The substrate composes.** Two paired end-to-end tests in
`internal/api/agent_substrate_e2e_test.go` boot the full router
stack and walk discovery, triage, depth, and the operator-state
write loop through the actual HTTP boundary. They are the
substantive proof that the four axes work as one.
- **Discovery is unauthenticated.** Pinned by
`TestContract_AgentCapabilitiesManifestIsPublic` after a slice 47
fix added the path to `publicPaths`. Slice 40 had it 401'ing
despite the docs.
- **Stable error envelope is two-layer.** Capability-specific codes
(`resource_not_found`, `operator_state_not_set`,
`operator_state_invalid`) are declared per-capability in the
manifest. Cross-cutting codes (`invalid_org`, `org_suspended`,
`access_denied`) come from the auth and multi-tenant middleware
and apply to every authenticated endpoint. Documented in
`api-contracts.md`; a contract test enforces no drift.
## Where to read more
- Full contract: `docs/release-control/v6/internal/subsystems/api-contracts.md`,
agent-surface paragraphs in the `## Current State` section.
- Implementation: `internal/api/agent_*.go` and
`internal/api/resources_operator_state.go`.
- MCP adapter: `cmd/pulse-mcp/` (with README).
- HTTP worked example: `cmd/agent-probe/`.
- Subsystem dependencies: relevant paragraphs in `agent-lifecycle.md`,
`performance-and-scalability.md`, and `storage-recovery.md` under
`docs/release-control/v6/internal/subsystems/`.