Commit Graph

22 Commits

Author SHA1 Message Date
rcourtman 1eb736e359 Add branch-coverage tests for provider tool-artifact leak detectors
New table-driven tests raise branch coverage on
SplitTrailingProviderToolNamePrefix and the JSON and plain function
tool-call leak-index helpers, covering empty content, no-alnum tails,
prefix-hold versus pass-through and regex no-match paths. Test-only.
2026-07-15 15:11:02 +01:00
rcourtman a95edafcf1 Qualify model-led Patrol investigations 2026-07-15 10:32:16 +01:00
rcourtman 280a44ef08 Add branch-coverage tests for AI patrol, qualification, tools and agentcapability helpers
New white-box table tests over previously-untested pure functions in
internal/ai (patrol run recency, patrol findings JSON validation),
internal/ai/tools (read-only violation hints, VM config parsing, node
target matching), internal/ai/qualification (compare predicate,
percentile and model summary, runner helpers), internal/ai/chat
(investigation run error, session compaction formatting) and
internal/agentcapabilities (path parameter substitution, markdown
helpers).

Twelve files, tests only, no source changes. All gates green with go
test, gofmt and go vet, plus an adversarial GLM review scoring twelve
KEEP and zero reject.
2026-07-14 22:33:40 +01:00
rcourtman 3f45953866 Complete Patrol autonomous qualification loop 2026-07-14 15:35:48 +01:00
rcourtman 4c073d6b17 Add mock action lifecycle data 2026-07-13 16:24:06 +01:00
rcourtman cd7886aa44 Enforce server-owned action approval authority 2026-07-11 19:49:17 +01:00
rcourtman 71ea14e4dd Add durable action dispatch continuity 2026-07-11 16:08:44 +01:00
rcourtman f510b99095 Enforce the canonical mutation plane 2026-07-11 15:19:41 +01:00
rcourtman ffd1ea8127 Enforce Assistant read-only scope integrity 2026-07-11 12:43:55 +01:00
rcourtman 4edf5f8265 fix: close chat command authority boundary 2026-07-11 11:54:56 +01:00
rcourtman 5bbfce956d Complete Patrol action lifecycle continuity 2026-07-10 21:22:06 +01:00
rcourtman 956f5749d6 A.1: close the proposal-channel contract breaks before Commit B
Privacy: provider-streamed RawInput overrides on tool_progress events
are unredacted model output; for exposure-restricted tools they are now
discarded instead of replacing the projected form (the override was
reintroducing exactly the values the projector removed). Proven with a
progress event carrying a secret in the raw override.

Schema validation: proposal acceptance now validates through the
planner's exported canonical rules - FindCapability's exact-name
matching (the capture previously matched case-insensitively while
planning matched exactly) and ValidateParams for declared, required,
typed, enum, pattern, and malformed-schema cases - so a proposal that
validates is exactly a proposal the planner will accept. The
sensitive-parameter rejection remains a proposal-specific ratchet on
top.

Fail-closed ratchets: params and evidence identity are deep-cloned on
capture and again on outcome, so caller-side mutation after validation
can never alter the actionable proposal; fingerprint serialization
failures return errors rather than a shared sentinel value; an
investigation run refuses to start without finding and investigation
identity (before any provider call or session exists); and any run
error nils the proposal while preserving simultaneous proposal errors
via errors.Join - a non-nil proposal exists only from a completely
successful run.
2026-07-10 17:08:25 +01:00
rcourtman 9bc51a8a35 Build the narrow proposal channel: patrol_propose_action and its capture sink
Commit A of the coordinated proposal slice (pulse-internal; the
aicontracts interface replacement and enterprise migration follow as
one window because pulse-enterprise builds against this tree via a
replace directive).

patrol_propose_action is a side-effect-free, mutation-none capture
whose schema carries only resource_id, capability_name, params, and
reason. Registry policy rejects it outside the Patrol investigation
profile before the handler runs, and the same check keeps it out of
every other profile's projected manifest. Correlation identity
(proposal, finding, investigation, evidence) is injected from trusted
orchestration context through the request-local ProposalCapture sink,
which executor clones share so one run has exactly one capture.

Tool calls now carry an explicit invocation envelope (tool-use ID,
name, arguments) through ExecuteInvocation; the ID rides the context
because per-turn tool calls execute concurrently. The sink keys on call
identity plus payload fingerprint: idempotent replay re-succeeds, the
same ID with a different payload latches a terminal integrity error,
and a second distinct valid proposal latches terminal ambiguity - both
terminal states invalidate the captured proposal, since concurrency
makes first-wins nondeterministic. Proposals count only after catalog
validation: advertised capability, declared/required/enum parameters,
and sensitive parameters rejected before success with no value echo in
any output.

ExecuteInvestigationStream returns proposal cardinality as a structured
result with typed errors for ambiguity, integrity violations, and the
failed-attempts-only case (never collapsed into the valid zero-proposal
conclusion); ListInvestigationTools projects through the identical
profile path. Proposal parameter values exist only transiently for
provider continuation and validation: the canonical exposure projector
redacts them from the durable transcript and every
tool_start/progress/end stream event, proven end-to-end with a scripted
provider run that also verifies the provider continuation keeps raw
values.

Essential proof included: two concurrent valid proposal calls produce
ErrProposalAmbiguous and a nil proposal regardless of execution order.
2026-07-10 16:47:23 +01:00
rcourtman 97a30b0600 Introduce the Patrol execution profiles: detection and investigation
Execution posture is now profile-owned through the core-only,
never-serialized tools.ExecutionProfile. Both Patrol profiles are
non-interactive, deny infrastructure mutations, and clear any inherited
autonomous mode. Detection restricts pulse-state mutations to an
explicit allowlist of the finding lifecycle tools - a blanket
pulse-state allowance would also permit alert dismissal and knowledge
writes - while investigation denies all pulse-state mutations, keeping
it structurally read-only. InvocationPolicy gains the allowlist and
Allows() is now tool-name-aware.

Chat turns build ONE effective request executor (control level,
autonomy, profile, resolved context) BEFORE provider projection and
clone that executor per provider attempt, reversing the previous
project-from-base-then-clone order so the offered schema and the
runtime boundary always agree. Scheduled Patrol (ExecutePatrolStream)
now runs under the detection profile instead of autonomous mode -
closing its direct view of Docker/Kubernetes mutation subactions - and
ListAvailableTools projects through the identical path.
toolsForExecutionMode's mode booleans are replaced by the profile on
the executor itself.

Non-interactive profiles independently hide pulse_question from the
manifest AND runtime-block it before the interactive-call-set special
case: a fabricated question call returns a non-interactive error
without emitting a waiting event, and sibling tool calls from the same
provider turn keep processing. Approval waits never block for
non-interactive profiles (they queue), and the tool-only-turn wrap-up
guardrail is interactive-profile-owned instead of keyed on autonomy.
The system prompt describes detection and investigation modes directly
rather than claiming controlled or autonomous execution; the
investigation prompt directs the model toward typed action proposals.

Proofs: detection allowlist enforcement (alerts resolve and knowledge
remember blocked, finding tools allowed, projection agrees),
investigation structural read-only (patrol mutation tools dropped from
the manifest and blocked at runtime), profile clone isolation, profile
prompt modes, and question-tool hiding across profiles.
2026-07-10 15:53:38 +01:00
rcourtman 3073a50614 Harden the invocation-descriptor contract before the investigation profile
Four classifier ratchets from review of 67c2534c0:

The classification vocabulary is closed: descriptor validation rejects
any class outside the known workflow kinds and mutation targets (an
empty Mutation no longer registers), and InvocationPolicy.Allows
independently denies unknown mutation targets outright, so a class
that somehow bypassed validation still cannot execute. Descriptor
lookups and registration store deep copies, so callers can never
mutate the canonical table through shared case maps or static class
pointers. Registration rejects descriptor overrides for canonical tool
names; overrides exist only for genuinely non-canonical extension and
test names.

Projected governance now derives its action mode from mutation
targets, not workflow kinds, and recomputes it even when no enum value
was filtered; a projection whose remaining invocations mutate nothing
downgrades to scope-only approval metadata (registered scope-only
summaries are preserved). Docker check_updates reclassifies from
{write,none} to {read,none}: it queues a read-only scan, and the write
kind was driving the FSM into verification and making the read-only
Docker projection report mixed. Discovery consequently projects as
mode=read in governance manifests, which is the honest mutation-derived
mode. The pulse_file_edit governance summary no longer claims to read
files. The contract prose also names pulse_read's execution-intent
classifier as mandatory second-stage enforcement for exec, not merely
defense in depth: the static read/none descriptor cannot prove
arbitrary command text safe.

Proofs: open-vocabulary rejection, unknown-target policy denial,
descriptor copy isolation, canonical-override rejection, and the
read-only Docker read/scope-only projection.
2026-07-10 13:53:31 +01:00
rcourtman 67c2534c08 Replace the hard-coded tool classifier with registry-owned invocation descriptors
Every registered Pulse tool now carries a canonical invocation
descriptor (internal/agentcapabilities/invocation.go): static or
discriminator-based, classifying each invocation with a workflow kind
plus a mutation target (none / pulse_state / infrastructure). Mixed
descriptors must exactly cover their schema enum and registration
panics otherwise, so an unclassifiable tool cannot exist. Missing,
malformed, unknown, or fabricated discriminator values classify
fail-closed as infrastructure writes.

Provider projection and runtime enforcement consume the same
descriptor under one InvocationPolicy (control level plus the
request-local, non-serializable deny_infrastructure_mutations
restriction, isolated across executor clones): ListTools and
ListToolGovernance remove forbidden enum values, drop empty tools, and
recompute the offered action mode, while ToolRegistry.Execute blocks
forbidden invocations before the handler runs. This closes the mixed
tool control-level bypass, most seriously Docker action:update, which
previously fell through to direct execution at read-only, and fixes
the Kubernetes misclassification: the retired switch read the action
argument while the schema discriminator is type, so type:scale
classified as read.

pulse_file_edit is now write-only (append/write); file inspection
routes through pulse_read action=file, whose exec path keeps its
structural read-only execution-intent enforcement. ClassifyToolCall
consults the descriptor table first and retains only genuinely
non-registry compatibility cases. The deny restriction is deliberately
separate from autonomous mode, which only suppresses interactive
questions and grants no mutation authority.

Proofs: descriptor validation and fail-closed classification unit
tests, plus the invocation-policy regression suite (scale classifies
write and never invokes at read-only or under deny; Docker update
queues nothing at read-only; autonomous plus deny cannot mutate;
fabricated enum values fail at runtime; filtered projection and
runtime enforcement agree; executor clones keep request policies
isolated). Contracts and registry ownership updated for the new
shared invocation descriptor boundary.

Slice 3a of the typed-lifecycle ratchet; the patrol_investigation
execution profile and patrol_propose_action tool build on this
substrate next.
2026-07-10 13:31:11 +01:00
rcourtman 8d07089eb3 Finish the Assistant & Patrol settings rename to Pulse Intelligence
Commit cc948b022 fixed the Patrol preflight strings that gated keyless
onboarding; this sweeps the rest. Error messages, readiness checks, and
guidance copy that still pointed at the retired Assistant & Patrol
settings page now name the real surfaces: Pulse Intelligence settings
for the area as a whole, and the Provider & Models settings page for
provider-credential guidance, matching the phrasing cc948b022
established. Pinning tests updated in step, including three
ai_handlers_test.go assertions cc948b022 had already left stale.
2026-07-10 12:04:03 +01:00
rcourtman f4c2fd0c38 Fail closed on unknown remediation lock state for autonomous dispatches
The AI action broker treated an unreadable operator lock as unlocked:
isResourceRemediationLocked returned (false, nil) with no audit store
wired, and the caller logged store errors then dispatched anyway. An
operator's NeverAutoRemediate=true could be silently ignored whenever
the policy store was missing or erroring, which is unacceptable while
Patrol and Assistant run at assisted or full autonomy.

Posture change at the dispatch decision point:
- isResourceRemediationLocked now reports unknown state (nil store or
  lookup failure) as an ErrRemediationLockStateUnknown-wrapped error
  instead of silently defaulting to unlocked.
- New checkRemediationLockForDispatch gate: dispatches without an
  approved human decision fail CLOSED on unknown lock state and
  surface "remediation lock state unknown; operator approval
  required". Human-approved dispatches keep the historical fail-open
  behavior with a warning log. A confirmed lock still refuses even
  approved dispatches, as before.
- executeNativeActionWithAudit (TrueNAS app start/stop/restart) now
  enforces the lock too; it previously skipped the check entirely.
- Refusals persist Failed audit records with stable
  remediation_lock_state_unknown: / resource_remediation_locked:
  ErrorMessage prefixes.
- ai-runtime subsystem contract updated to pin the new posture.

Tests cover store-error and nil-store at both autonomy postures on
both dispatch paths; routing/control tests now wire an in-memory
audit store since autonomous dispatch without one is refused.
2026-07-10 00:14:01 +01:00
rcourtman 313648b2f1 feat(mcp): add additive fleet-context filtering
get_fleet_context returned the whole registry with no way to narrow it,
so a large fleet produced a payload that exceeded the agent harness's
50KB cap and forced agents to receive or page through healthy resources
that are pure noise from a triage standpoint.

Add optional additive query-param filters — hasFindings, severity,
technology, resourceType — that compose by intersection. All optional
(omitting every filter returns the full fleet, backward compatible);
unknown/unmatched values return 200 with resources: [] (a valid triage
answer, not an error). The hasFindings=true filter is the headline
triage case: show me only what needs attention.

The MCP adapter layer had no GET query-param transport — ProjectCapabilityCall
short-circuited GET after path substitution and dropped all other args.
Close that gap generically: ProjectedCall gains a Query url.Values field
populated from leftover non-path public args for GET/DELETE, and
BuildCapabilityHTTPRequest encodes it onto the request URL. Benefits any
future GET capability with filter args, not just fleet-context. The
resources/list adapter calls fleet-context with empty args and continues
to receive the unfiltered fleet.

The manifest now declares the filters via inputSchema so they are
discoverable through the capability surface the same way add_node's
params are.

- ProjectedCall.Query + GET query-param forwarding (projection.go, http.go)
- fleetContextFilter parsing/matching in HandleFleetContext
- fleetContextInputSchema + InputSchema on the capability
- Regenerated cmd/pulse-mcp/README.md via generate-pulse-intelligence-docs
- Contract updates: api-contracts, ai-runtime, agent-lifecycle, storage-recovery
- Tests: adapter query-forwarding unit tests, 7 filter handler tests
  (hasFindings/severity/technology/resourceType/no-filter/unknown/compose),
  contract pinning inputSchema + end-to-end query forwarding
2026-07-03 18:01:05 +01:00
rcourtman 20c2f27057 feat(mcp): add list_resource_capabilities tool
Agents had no way to discover which governed capabilities a resource
advertises before calling plan_action. The data existed (Resource.Capabilities
with full param schemas) but was only surfaced as count-limited prose
facts inside get_resource_context, which deliberately omits parameter
schemas. This forced agents to guess capabilityName and params.

Add a dedicated structured surface: GET /api/agent/resource-capabilities/{id}
returns the canonical ResourceCapability list (name, type, approval level,
platform, full parameter schemas) for a single resource. A resource with
no advertised capabilities returns 200 with an empty array, the signal
to skip plan_action.

Internal plumbing already existed (registry.Get + Resource.Capabilities,
the same path plan_action validates against); this is the wiring work at
the canonical agent-surface layer. The tools/call dispatch is fully
generic so no mcp.go changes were needed.

Companion to get_resource_context: that tool remains the human-readable
prose summary; this is the structured schema surface for action planning.

- Manifest capability, name/path constants, output schema helper
- Handler mirroring HandleResourceContext error/scope patterns
- Route registration behind monitoring:read
- Activity telemetry mapping to resource_context class
- Regenerated cmd/pulse-mcp/README.md via generate-pulse-intelligence-docs
- Contract updates: api-contracts, ai-runtime, agent-lifecycle, storage-recovery
- Tests: manifest ownership/addressing/table-test, handler happy/empty/404/405,
  contract pinning capability presence/scope/wire-shape
2026-07-03 17:07:41 +01:00
rcourtman 6dce36fbab Clarify Patrol current-work copy 2026-06-25 14:37:04 +01:00
rcourtman ee8a24e14a backend and governance: MCP contract, agent capabilities, API, and release-control
Manifest-backed MCP tools, prompts, and resources with surface affordance contracts; agent capability manifest and governance projection; API contract tests and capability route projection; operations-loop and intelligence-funnel telemetry; release-control subsystem documentation, registry, and tooling; licensing and configuration.
2026-06-23 17:26:15 +01:00