mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
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.
423 lines
17 KiB
Go
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
|
|
}
|