Pulse/internal/agentcapabilities/types.go
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

423 lines
17 KiB
Go

package agentcapabilities
import (
"encoding/json"
"net/http"
"strings"
)
// ActionMode tells agents whether a capability or tool reads state, can run a
// non-persistent check/scan, or changes Pulse or target-side state.
type ActionMode string
// ApprovalPolicy tells external agents whether the required scope is sufficient
// or whether the capability participates in Pulse's governed action-plan
// lifecycle.
type ApprovalPolicy string
const (
ActionModeRead ActionMode = "read"
ActionModeMixed ActionMode = "mixed"
ActionModeWrite ActionMode = "write"
ApprovalPolicyScopeOnly ApprovalPolicy = "scope_only"
ApprovalPolicyActionPlan ApprovalPolicy = "action_plan"
)
// ControlToolsDisabledMessage is the stable operator guidance returned when a
// caller attempts to run a control-gated Assistant tool while the shared control
// level is read-only or otherwise not allowed to expose control tools.
const ControlToolsDisabledMessage = "Control tools are disabled. Open Assistant & Patrol settings, then set Pulse Assistant Permissions > Control mode to Controlled before using action tools."
// DefaultApprovalPolicyDescription returns a concise operator-facing
// explanation for a shared approval-policy value.
func DefaultApprovalPolicyDescription(policy ApprovalPolicy) string {
switch policy {
case ApprovalPolicyScopeOnly:
return "required scope is sufficient; no action-plan approval is required"
case ApprovalPolicyActionPlan:
return "governed action-plan approval is required before execution"
default:
return ""
}
}
// NormalizeToolGovernance fills the shared governance defaults for a tool
// definition. Assistant registry tools and external-agent projections use this
// instead of carrying their own action-mode and approval-policy fallback rules.
func NormalizeToolGovernance(governance ToolGovernance, requireControl bool, description string) ToolGovernance {
if governance.ActionMode == "" {
if requireControl {
governance.ActionMode = ActionModeWrite
} else {
governance.ActionMode = ActionModeRead
}
}
if governance.ApprovalPolicy == "" {
if governance.ActionMode == ActionModeRead {
governance.ApprovalPolicy = ApprovalPolicyScopeOnly
} else if requireControl {
governance.ApprovalPolicy = ApprovalPolicyActionPlan
} else {
governance.ApprovalPolicy = ApprovalPolicyScopeOnly
}
}
if governance.ApprovalSummary == "" {
if governance.ActionMode == ActionModeRead {
governance.ApprovalSummary = "no approval required"
} else if requireControl {
governance.ApprovalSummary = "hidden in read-only mode; approval required in controlled mode"
} else if desc := DefaultApprovalPolicyDescription(governance.ApprovalPolicy); desc != "" {
governance.ApprovalSummary = desc
} else {
governance.ApprovalSummary = "write subactions require the tool's governed approval path"
}
}
if governance.Summary == "" {
governance.Summary = description
}
return governance
}
// NormalizeCapabilityGovernance fills the shared governance defaults for a
// manifest capability. Capability projections use HTTP method only as a fallback
// when the manifest has not declared an explicit action mode; the manifest-owned
// actionMode and approvalPolicy remain authoritative when present.
func NormalizeCapabilityGovernance(cap Capability) ToolGovernance {
governance := ToolGovernance{
ActionMode: CapabilityActionMode(cap),
ApprovalPolicy: cap.ApprovalPolicy,
Summary: cap.Description,
}
return NormalizeToolGovernance(governance, false, cap.Description)
}
// CapabilityActionMode returns the shared action mode for a manifest
// capability, preserving explicit read/mixed/write values and using method
// posture only as a compatibility fallback for incomplete manifests.
func CapabilityActionMode(cap Capability) ActionMode {
switch mode := ActionMode(strings.TrimSpace(string(cap.ActionMode))); mode {
case ActionModeRead, ActionModeMixed, ActionModeWrite:
return mode
}
if strings.EqualFold(cap.Method, http.MethodGet) {
return ActionModeRead
}
return ActionModeWrite
}
// NewToolGovernanceDescriptor builds the shared read-only governance descriptor
// for a registered tool after applying the canonical defaults.
func NewToolGovernanceDescriptor(name, description string, requireControl bool, governance ToolGovernance) ToolGovernanceDescriptor {
normalized := NormalizeToolGovernance(governance, requireControl, description)
return ToolGovernanceDescriptor{
Name: name,
Description: description,
RequireControl: requireControl,
ActionMode: normalized.ActionMode,
ApprovalPolicy: normalized.ApprovalPolicy,
ApprovalSummary: normalized.ApprovalSummary,
Summary: normalized.Summary,
}
}
// NormalizeToolGovernanceDescriptor applies the shared governance defaults to
// an existing descriptor without changing its identity or control-gating flag.
func NormalizeToolGovernanceDescriptor(descriptor ToolGovernanceDescriptor) ToolGovernanceDescriptor {
return NewToolGovernanceDescriptor(descriptor.Name, descriptor.Description, descriptor.RequireControl, ToolGovernance{
ActionMode: descriptor.ActionMode,
ApprovalPolicy: descriptor.ApprovalPolicy,
ApprovalSummary: descriptor.ApprovalSummary,
Summary: descriptor.Summary,
})
}
// ToolGovernance records the governed posture for a structured tool. Concrete
// surfaces own handlers and availability, while this shared shape keeps
// Assistant prompts and external-agent projections on the same action and
// approval vocabulary.
type ToolGovernance struct {
ActionMode ActionMode
ApprovalPolicy ApprovalPolicy
ApprovalSummary string
Summary string
}
// ToolGovernanceDescriptor is the read-only tool governance manifest surfaced
// to agents and Assistant prompt builders.
type ToolGovernanceDescriptor struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
RequireControl bool `json:"requireControl,omitempty"`
ActionMode ActionMode `json:"actionMode"`
ApprovalPolicy ApprovalPolicy `json:"approvalPolicy"`
ApprovalSummary string `json:"approvalSummary,omitempty"`
Summary string `json:"summary,omitempty"`
}
// Capability declares one agent-consumable Pulse capability: what it does,
// which canonical HTTP route it calls, which scope it needs, how it is governed,
// and which stable error codes agents can branch on. The shape is intentionally
// narrow so MCP, in-app Assistant, and future external-agent adapters can
// project the same manifest without a second local registry.
type Capability struct {
// Name is the agent-stable snake_case identifier. Renaming breaks
// integrations and requires a manifest-version decision.
Name string `json:"name"`
// Title is the human-readable display name for agent clients. Name remains
// the programmatic identifier; UI, docs, and MCP tool titles use this field.
Title string `json:"title,omitempty"`
// Description is a single-sentence explanation an agent can surface when
// deciding whether to call this capability.
Description string `json:"description"`
// Category groups capabilities for agent UIs. Stable values include
// "context", "provisioning", "operator-state", "finding", and "action".
Category string `json:"category"`
// Method and Path describe the canonical REST surface. Path segments in
// braces are agent-supplied parameters and must be percent-encoded as
// single path segments by adapters.
Method string `json:"method"`
Path string `json:"path"`
// Scope names the auth scope required to call this capability.
Scope string `json:"scope"`
// ActionMode tells agents whether the capability reads state, can run a
// non-persistent check/scan, or changes Pulse or target-side state.
ActionMode ActionMode `json:"actionMode"`
// ApprovalPolicy tells agents whether the required scope is sufficient or
// whether the capability participates in the governed action-plan lifecycle.
ApprovalPolicy ApprovalPolicy `json:"approvalPolicy"`
// ResponseShape is the agent-stable response type name.
ResponseShape string `json:"responseShape,omitempty"`
// OutputSchema is an optional JSON Schema for the structured result object
// returned by request/response tools. It is raw JSON so the API can serve it
// verbatim and MCP adapters can forward the exact same schema.
OutputSchema json.RawMessage `json:"outputSchema,omitempty"`
// ErrorCodes is the closed set of stable error codes this capability may
// return on failure. Agents branch on these, not on human messages.
ErrorCodes []string `json:"errorCodes,omitempty"`
// RequestBodyShape names the agent-stable request body shape for non-GET
// capabilities.
RequestBodyShape string `json:"requestBodyShape,omitempty"`
// InputSchema is an optional JSON Schema for agent-facing tool arguments.
// It is raw JSON so the API can serve it verbatim and adapters can forward
// the exact same schema.
InputSchema json.RawMessage `json:"inputSchema,omitempty"`
}
// CapabilityCategory describes one manifest-owned capability group for agent
// UIs and generated MCP docs. The category IDs on individual capabilities must
// refer to this vocabulary.
type CapabilityCategory struct {
ID string `json:"id"`
Label string `json:"label"`
Description string `json:"description,omitempty"`
}
// SurfaceContractComponent names one singular part of the shared Pulse
// Intelligence runtime contract, such as the shared core or primary built-in
// operator.
type SurfaceContractComponent struct {
ID string `json:"id"`
Label string `json:"label"`
Description string `json:"description"`
}
// SurfaceAffordanceContract declares which Pulse Intelligence affordances a
// supported operator-facing surface owns. The manifest carries these booleans
// so Assistant, MCP, docs, and settings UI do not hand-maintain separate
// surface capability claims.
type SurfaceAffordanceContract struct {
Tools bool `json:"tools,omitempty"`
Resources bool `json:"resources,omitempty"`
Prompts bool `json:"prompts,omitempty"`
CapabilityMetadata bool `json:"capabilityMetadata,omitempty"`
InteractiveQuestions bool `json:"interactiveQuestions,omitempty"`
}
// OperatorSurfaceContract describes one supported access path over the shared
// Pulse Intelligence Core. The JSON name remains operatorSurfaces for v1
// manifest compatibility.
type OperatorSurfaceContract struct {
ID string `json:"id"`
Label string `json:"label"`
Description string `json:"description"`
Native bool `json:"native"`
ExternalAdapter bool `json:"externalAdapter"`
Affordances SurfaceAffordanceContract `json:"affordances,omitempty"`
}
// SurfaceContract pins the runtime relationship between the shared Pulse
// Intelligence Core, the primary built-in Patrol operator, and the supported
// access paths. External agents consume this from the same discovery manifest
// as tool capabilities so Pulse MCP cannot drift into a separate product
// contract.
type SurfaceContract struct {
Core SurfaceContractComponent `json:"core"`
ProactiveEngine SurfaceContractComponent `json:"proactiveEngine"`
OperatorSurfaces []OperatorSurfaceContract `json:"operatorSurfaces"`
}
// MCPAdapterConfigFamily describes one client configuration shape supported by
// the Pulse MCP adapter. Client labels are examples for operator-facing copy;
// Shape is the stable formatter key used by docs and UI projections.
type MCPAdapterConfigFamily struct {
ID string `json:"id"`
Label string `json:"label"`
Shape string `json:"shape"`
Description string `json:"description,omitempty"`
FileHints []string `json:"fileHints,omitempty"`
ClientLabels []string `json:"clientLabels,omitempty"`
}
// MCPAdapterContract pins the runtime facts that every Pulse MCP client setup
// must share. Keeping these values in the manifest prevents the first-party UI,
// README generation, and external adapters from hand-maintaining separate
// command, flag, token, and config-shape copies.
type MCPAdapterContract struct {
ServerName string `json:"serverName"`
Command string `json:"command"`
BaseURLFlag string `json:"baseUrlFlag"`
DefaultBaseURL string `json:"defaultBaseUrl"`
TokenEnv string `json:"tokenEnv"`
ConfigFamilies []MCPAdapterConfigFamily `json:"configFamilies"`
}
// Manifest is the discovery document for Pulse's agent surface. The API serves
// this shape directly, while adapters such as pulse-mcp and agent-probe consume
// the same type rather than maintaining local wire-shape copies.
type Manifest struct {
Version string `json:"version"`
// SurfaceContract is the canonical product/runtime relationship for Pulse
// Intelligence surfaces. It keeps first-party Assistant, external-agent MCP,
// and proactive Patrol wording coupled to the same discovery contract as the
// manifest-backed tools.
SurfaceContract SurfaceContract `json:"surfaceContract"`
// SurfaceToolContracts is the manifest-owned static tool posture for
// external-agent adapters. Native Assistant live tool availability is
// runtime-specific and stays behind the authenticated Assistant endpoint.
SurfaceToolContracts []SurfaceToolContract `json:"surfaceToolContracts"`
// MCPAdapter is the canonical setup contract for the Pulse MCP adapter:
// server name, command, base URL flag/default, token environment variable,
// and the supported client configuration families.
MCPAdapter MCPAdapterContract `json:"mcpAdapter"`
// RequiredScopes is the canonical deduplicated scope set needed to use every
// currently declared capability.
RequiredScopes []string `json:"requiredScopes"`
// Categories is the manifest-owned presentation order and copy for
// capability groups. First-party UI, MCP docs, and external clients consume
// this instead of carrying local category tables.
Categories []CapabilityCategory `json:"categories"`
// WorkflowPrompts is the manifest-owned reusable workflow starter metadata.
// Native Assistant surfaces and external MCP clients consume this same list
// instead of maintaining prompt catalogues per surface.
WorkflowPrompts []PulseWorkflowPrompt `json:"workflowPrompts"`
Capabilities []Capability `json:"capabilities"`
}
// CloneCapability returns a detached capability descriptor so callers cannot
// mutate manifest-owned collection fields through a lookup or projection result.
func CloneCapability(cap Capability) Capability {
if len(cap.ErrorCodes) > 0 {
cap.ErrorCodes = append([]string(nil), cap.ErrorCodes...)
}
cap.InputSchema = CloneRawMessage(cap.InputSchema)
cap.OutputSchema = CloneRawMessage(cap.OutputSchema)
return cap
}
// CloneCapabilities returns detached capability descriptors in the original
// order.
func CloneCapabilities(capabilities []Capability) []Capability {
if capabilities == nil {
return nil
}
cloned := make([]Capability, len(capabilities))
for i, cap := range capabilities {
cloned[i] = CloneCapability(cap)
}
return cloned
}
// CloneCapabilityCategories returns detached capability-category descriptors in
// the original manifest order.
func CloneCapabilityCategories(categories []CapabilityCategory) []CapabilityCategory {
if categories == nil {
return nil
}
cloned := make([]CapabilityCategory, len(categories))
copy(cloned, categories)
return cloned
}
// ClonePulseWorkflowPrompt returns a detached workflow prompt descriptor.
func ClonePulseWorkflowPrompt(prompt PulseWorkflowPrompt) PulseWorkflowPrompt {
prompt.Arguments = append([]PulseWorkflowPromptArgument(nil), prompt.Arguments...)
return prompt.NormalizeCollections()
}
// ClonePulseWorkflowPrompts returns detached workflow prompt descriptors in the
// original projection order.
func ClonePulseWorkflowPrompts(prompts []PulseWorkflowPrompt) []PulseWorkflowPrompt {
if prompts == nil {
return nil
}
cloned := make([]PulseWorkflowPrompt, len(prompts))
for i, prompt := range prompts {
cloned[i] = ClonePulseWorkflowPrompt(prompt)
}
return cloned
}
// CloneSurfaceContract returns a detached copy of the manifest surface
// relationship contract.
func CloneSurfaceContract(contract SurfaceContract) SurfaceContract {
if contract.OperatorSurfaces != nil {
contract.OperatorSurfaces = append([]OperatorSurfaceContract(nil), contract.OperatorSurfaces...)
}
return contract
}
// CloneMCPAdapterConfigFamilies returns detached MCP adapter configuration
// family descriptors.
func CloneMCPAdapterConfigFamilies(families []MCPAdapterConfigFamily) []MCPAdapterConfigFamily {
if families == nil {
return nil
}
cloned := make([]MCPAdapterConfigFamily, len(families))
for i, family := range families {
cloned[i] = family
if family.FileHints != nil {
cloned[i].FileHints = append([]string(nil), family.FileHints...)
}
if family.ClientLabels != nil {
cloned[i].ClientLabels = append([]string(nil), family.ClientLabels...)
}
}
return cloned
}
// CloneMCPAdapterContract returns a detached copy of the manifest-owned Pulse
// MCP adapter setup contract.
func CloneMCPAdapterContract(contract MCPAdapterContract) MCPAdapterContract {
contract.ConfigFamilies = CloneMCPAdapterConfigFamilies(contract.ConfigFamilies)
return contract
}