mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 02:28:59 +00:00
Project Patrol tools from resource scope
This commit is contained in:
parent
2a15ccbefa
commit
80027e98d6
19 changed files with 428 additions and 33 deletions
|
|
@ -6302,3 +6302,11 @@ detection manifest and model-owned Pulse-state writes. They do not
|
||||||
enroll an agent, select an agent identity, enable commands, grant a command
|
enroll an agent, select an agent identity, enable commands, grant a command
|
||||||
session, alter fleet policy, or bypass agent-side preflight and action
|
session, alter fleet policy, or bypass agent-side preflight and action
|
||||||
admission. Agent lifecycle and command authority remain unchanged.
|
admission. Agent lifecycle and command authority remain unchanged.
|
||||||
|
|
||||||
|
Exact scoped Watch and investigation requests may also carry a core-resolved
|
||||||
|
canonical resource type through the shared AI handler boundary. The type only
|
||||||
|
selects a smaller read-only provider-tool projection after the Patrol profile
|
||||||
|
has been applied; it cannot select an agent, add an agent-routed tool, establish
|
||||||
|
a command session, or bypass capability, feasibility, policy, approval,
|
||||||
|
dispatch, receipt, and verification gates. Unknown types retain the existing
|
||||||
|
governed Patrol profile rather than guessing at agent authority.
|
||||||
|
|
|
||||||
|
|
@ -653,6 +653,7 @@ cheap local detection into model-owned diagnosis and governed action.
|
||||||
18. `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/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.
|
||||||
19. `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.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.
|
||||||
20. `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/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.
|
||||||
|
21. `internal/agentcapabilities/patrol_scope_tools.go` shared with `api-contracts`: the typed Patrol resource-scope evidence mapping is both the AI runtime least-manifest projection contract and the canonical API/agent vocabulary that keeps scoped Watch and investigation tool reductions aligned across the Pulse and enterprise boundary.
|
||||||
21. `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/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.
|
||||||
22. `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/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.
|
||||||
23. `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/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.
|
||||||
|
|
@ -7367,10 +7368,21 @@ change to the intent it was designed to protect.
|
||||||
|
|
||||||
### Patrol follow-up passes are exact-scope, least-manifest continuations
|
### Patrol follow-up passes are exact-scope, least-manifest continuations
|
||||||
|
|
||||||
Patrol's main Watch pass retains the full governed detection-profile manifest
|
An unscoped Patrol Watch retains the full governed detection-profile manifest
|
||||||
so the selected model owns evidence gathering and diagnosis, minus the
|
so the selected model owns evidence gathering and diagnosis, minus the
|
||||||
core-satisfied active-finding read. Bounded continuations do not repeat that
|
core-satisfied active-finding read. An explicitly scoped Watch whose exact
|
||||||
broad manifest: the unmatched-signal evaluation pass receives only
|
resource identity resolves to known canonical resource types receives the
|
||||||
|
least evidence manifest for those types plus the detection-owned finding
|
||||||
|
lifecycle. `internal/agentcapabilities/patrol_scope_tools.go` owns that mapping:
|
||||||
|
generic canonical query/read operations remain available for evidence-led
|
||||||
|
cross-resource causality, while unrelated subsystem schemas are removed. The
|
||||||
|
mapping consumes only caller-supplied or core-resolved typed scope, never prompt
|
||||||
|
text. An empty or unknown type keeps the full governed profile instead of
|
||||||
|
guessing and silently blinding Patrol; the allowlist is applied after profile
|
||||||
|
projection and therefore can remove tools but cannot add authority.
|
||||||
|
|
||||||
|
Bounded continuations do not repeat either main-pass manifest: the
|
||||||
|
unmatched-signal evaluation pass receives only
|
||||||
`patrol_report_finding` after core has established the complete exact-scope
|
`patrol_report_finding` after core has established the complete exact-scope
|
||||||
active-finding snapshot; narrow compatibility paths without a complete snapshot
|
active-finding snapshot; narrow compatibility paths without a complete snapshot
|
||||||
receive only `patrol_get_findings` plus `patrol_report_finding`. The
|
receive only `patrol_get_findings` plus `patrol_report_finding`. The
|
||||||
|
|
@ -7429,6 +7441,17 @@ finding-lifecycle retries suppressed before canonical invocation are runtime
|
||||||
loop-control decisions rather than tool executions and do not create a second
|
loop-control decisions rather than tool executions and do not create a second
|
||||||
tool-start/tool-end pair.
|
tool-start/tool-end pair.
|
||||||
|
|
||||||
|
An investigation carries the finding's trusted canonical `resource_type`
|
||||||
|
through `pkg/aicontracts/orchestrator_deps.go` into the core-owned chat runtime.
|
||||||
|
Core uses the same scoped evidence mapping to project generic query/read tools,
|
||||||
|
the relevant subsystem evidence tool, and only the investigation-owned
|
||||||
|
capability lookup and typed proposal sink. The type is not model-authored and
|
||||||
|
cannot add a tool outside the already-projected investigation profile. Unknown
|
||||||
|
types keep the full safe investigation profile. This keeps focused Patrol
|
||||||
|
usable with smaller local models without replacing model judgment with a
|
||||||
|
diagnostic script or weakening proposal, policy, approval, execution, or
|
||||||
|
verification boundaries.
|
||||||
|
|
||||||
### Patrol investigation fallback summaries remain evidence-grounded
|
### Patrol investigation fallback summaries remain evidence-grounded
|
||||||
|
|
||||||
When a provider completes an investigation tool loop but fails to emit the
|
When a provider completes an investigation tool loop but fails to emit the
|
||||||
|
|
|
||||||
|
|
@ -1415,6 +1415,7 @@ payload shape change when the portal presents compact client rows.
|
||||||
44. `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/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.
|
||||||
45. `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.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.
|
||||||
46. `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/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.
|
||||||
|
47. `internal/agentcapabilities/patrol_scope_tools.go` shared with `ai-runtime`: the typed Patrol resource-scope evidence mapping is both the AI runtime least-manifest projection contract and the canonical API/agent vocabulary that keeps scoped Watch and investigation tool reductions aligned across the Pulse and enterprise boundary.
|
||||||
47. `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/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.
|
||||||
48. `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/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.
|
||||||
49. `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/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.
|
||||||
|
|
@ -4150,6 +4151,18 @@ terminal proposal/final-summary capacity. An absent or zero persisted setting
|
||||||
uses the shipped ten-call default; explicit values retain the bounded 5–30
|
uses the shipped ten-call default; explicit values retain the bounded 5–30
|
||||||
contract.
|
contract.
|
||||||
|
|
||||||
|
The same boundary carries the finding's canonical `resource_type` as trusted
|
||||||
|
orchestrator metadata. Core consumes it only through
|
||||||
|
`internal/agentcapabilities/patrol_scope_tools.go` to reduce the already
|
||||||
|
governed investigation provider manifest to generic cross-resource reads, the
|
||||||
|
relevant subsystem evidence surface, and the investigation-owned capability
|
||||||
|
lookup/proposal tools. It is not a client-selectable execution profile, cannot
|
||||||
|
add authority, and is never inferred from prompt text. Empty or unknown types
|
||||||
|
retain the full governed investigation profile so an additive resource kind
|
||||||
|
cannot silently lose evidence access. Scoped Watch uses the same shared typed
|
||||||
|
mapping after exact canonical scope resolution, with detection lifecycle tools
|
||||||
|
composed separately from investigation proposal tools.
|
||||||
|
|
||||||
Unified Agent connections now carry agent-authored lifecycle evidence through
|
Unified Agent connections now carry agent-authored lifecycle evidence through
|
||||||
the shared host model and `/api/connections` contract. The payload may include
|
the shared host model and `/api/connections` contract. The payload may include
|
||||||
the applied managed-config fingerprint, self-update state and timestamps, and
|
the applied managed-config fingerprint, self-update state and timestamps, and
|
||||||
|
|
@ -9493,3 +9506,13 @@ authority when reached; zero preserves the ordinary uncapped Watch contract.
|
||||||
This is an internal execution contract, not a new HTTP field or client-granted
|
This is an internal execution contract, not a new HTTP field or client-granted
|
||||||
capability. Existing Patrol routes, authorization, tenant binding, payloads,
|
capability. Existing Patrol routes, authorization, tenant binding, payloads,
|
||||||
and action approval semantics are unchanged.
|
and action approval semantics are unchanged.
|
||||||
|
|
||||||
|
Exact scoped Watch runs may now populate that internal allowlist from the
|
||||||
|
core-resolved canonical resource type set. The shared
|
||||||
|
`agentcapabilities.PatrolDetectionToolNamesForResourceTypes` mapping retains
|
||||||
|
generic query/read evidence and only the relevant subsystem and detection
|
||||||
|
lifecycle schemas. Investigations carry their finding `resource_type` over the
|
||||||
|
public Pulse/Enterprise orchestrator dependency contract and use the companion
|
||||||
|
investigation projection. Both paths are reductions after execution-profile
|
||||||
|
projection; neither external clients nor model output can select or widen the
|
||||||
|
surface.
|
||||||
|
|
|
||||||
|
|
@ -612,6 +612,14 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"path": "internal/agentcapabilities/patrol_scope_tools.go",
|
||||||
|
"rationale": "the typed Patrol resource-scope evidence mapping is both the AI runtime least-manifest projection contract and the canonical API/agent vocabulary that keeps scoped Watch and investigation tool reductions aligned across the Pulse and enterprise boundary",
|
||||||
|
"subsystems": [
|
||||||
|
"ai-runtime",
|
||||||
|
"api-contracts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
"path": "internal/agentcapabilities/projection.go",
|
"path": "internal/agentcapabilities/projection.go",
|
||||||
"rationale": "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",
|
"rationale": "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",
|
||||||
"subsystems": [
|
"subsystems": [
|
||||||
|
|
|
||||||
|
|
@ -5245,3 +5245,11 @@ reductions. They add no storage, backup, snapshot, restore, retention, or
|
||||||
recovery tool and cannot turn a detection pass into a mutation path. Storage
|
recovery tool and cannot turn a detection pass into a mutation path. Storage
|
||||||
and recovery state, evidence freshness, persistence, and admission contracts
|
and recovery state, evidence freshness, persistence, and admission contracts
|
||||||
remain unchanged.
|
remain unchanged.
|
||||||
|
|
||||||
|
The shared AI handler may also forward the finding's canonical resource type
|
||||||
|
so core can reduce a scoped Watch or investigation to relevant read-only
|
||||||
|
evidence tools. That typed reduction cannot add a storage/recovery capability
|
||||||
|
outside the already-projected Patrol profile and grants no plan, approval,
|
||||||
|
execution, retention, cleanup, restore, or verification authority. An unknown
|
||||||
|
resource type keeps the normal governed profile instead of inferring storage
|
||||||
|
authority from prompt text.
|
||||||
|
|
|
||||||
90
internal/agentcapabilities/patrol_scope_tools.go
Normal file
90
internal/agentcapabilities/patrol_scope_tools.go
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
package agentcapabilities
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// PatrolEvidenceToolNamesForResourceTypes returns the smallest useful
|
||||||
|
// read-only evidence surface for an exact, core-resolved Patrol resource
|
||||||
|
// scope. The bool is false when the scope is empty or contains an unknown
|
||||||
|
// resource type; callers must then retain their normal governed profile
|
||||||
|
// rather than guessing from prompt text or accidentally hiding a capability.
|
||||||
|
//
|
||||||
|
// pulse_query and pulse_read are the cross-resource escape hatches: a focused
|
||||||
|
// investigation can still follow canonical topology and inspect a causal
|
||||||
|
// dependency without receiving every subsystem-specific schema up front.
|
||||||
|
func PatrolEvidenceToolNamesForResourceTypes(resourceTypes []string) ([]string, bool) {
|
||||||
|
if len(resourceTypes) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
names := []string{PulseQueryToolName, PulseReadToolName}
|
||||||
|
seen := map[string]bool{
|
||||||
|
PulseQueryToolName: true,
|
||||||
|
PulseReadToolName: true,
|
||||||
|
}
|
||||||
|
add := func(name string) {
|
||||||
|
if !seen[name] {
|
||||||
|
seen[name] = true
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
known := 0
|
||||||
|
for _, raw := range resourceTypes {
|
||||||
|
resourceType := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
if resourceType == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
known++
|
||||||
|
switch resourceType {
|
||||||
|
case "app-container", "app_container", "docker-container", "docker_container", "docker-host", "docker":
|
||||||
|
add(PulseDockerToolName)
|
||||||
|
case "storage", "storage-pool", "physical-disk", "physical_disk", "pbs", "backup":
|
||||||
|
add(PulseMetricsToolName)
|
||||||
|
add(PulseStorageToolName)
|
||||||
|
case "k8s-cluster", "k8s-node", "k8s-pod", "k8s-deployment", "kubernetes", "kubernetes-cluster":
|
||||||
|
add(PulseKubernetesToolName)
|
||||||
|
case "pmg":
|
||||||
|
add(PulsePMGToolName)
|
||||||
|
case "network-endpoint", "endpoint", "discovery":
|
||||||
|
add(PulseDiscoveryToolName)
|
||||||
|
case "agent", "host", "truenas", "node", "vm", "system-container", "container", "service":
|
||||||
|
add(PulseMetricsToolName)
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if known == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return names, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatrolDetectionToolNamesForResourceTypes composes the scoped evidence
|
||||||
|
// surface with the finding lifecycle owned by the detection profile. It may
|
||||||
|
// only reduce the already-governed profile manifest.
|
||||||
|
func PatrolDetectionToolNamesForResourceTypes(resourceTypes []string) ([]string, bool) {
|
||||||
|
names, ok := PatrolEvidenceToolNamesForResourceTypes(resourceTypes)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return append(names,
|
||||||
|
PatrolGetFindingsToolName,
|
||||||
|
PatrolReportFindingToolName,
|
||||||
|
PatrolAssessFindingToolName,
|
||||||
|
PatrolResolveFindingToolName,
|
||||||
|
), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatrolInvestigationToolNamesForResourceTypes composes scoped evidence with
|
||||||
|
// the side-effect-free capability lookup and typed proposal sink. Planning,
|
||||||
|
// approval, execution, and verification remain outside this surface.
|
||||||
|
func PatrolInvestigationToolNamesForResourceTypes(resourceTypes []string) ([]string, bool) {
|
||||||
|
names, ok := PatrolEvidenceToolNamesForResourceTypes(resourceTypes)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return append(names,
|
||||||
|
PatrolActionCapabilitiesToolName,
|
||||||
|
PatrolProposeActionToolName,
|
||||||
|
), true
|
||||||
|
}
|
||||||
58
internal/agentcapabilities/patrol_scope_tools_test.go
Normal file
58
internal/agentcapabilities/patrol_scope_tools_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package agentcapabilities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPatrolScopedToolProjectionUsesTypedLeastManifest(t *testing.T) {
|
||||||
|
dockerEvidence, ok := PatrolEvidenceToolNamesForResourceTypes([]string{"app-container"})
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("app-container scope was not recognized")
|
||||||
|
}
|
||||||
|
if !slices.Equal(dockerEvidence, []string{PulseQueryToolName, PulseReadToolName, PulseDockerToolName}) {
|
||||||
|
t.Fatalf("Docker evidence tools = %v", dockerEvidence)
|
||||||
|
}
|
||||||
|
for _, forbidden := range []string{PulseStorageToolName, PulseKubernetesToolName, PulsePMGToolName, PulseAlertsToolName, PulseKnowledgeToolName, PulseSummarizeToolName} {
|
||||||
|
if slices.Contains(dockerEvidence, forbidden) {
|
||||||
|
t.Fatalf("Docker evidence surface included unrelated tool %q: %v", forbidden, dockerEvidence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storageInvestigation, ok := PatrolInvestigationToolNamesForResourceTypes([]string{"physical-disk"})
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("physical-disk scope was not recognized")
|
||||||
|
}
|
||||||
|
for _, required := range []string{PulseQueryToolName, PulseReadToolName, PulseMetricsToolName, PulseStorageToolName, PatrolActionCapabilitiesToolName, PatrolProposeActionToolName} {
|
||||||
|
if !slices.Contains(storageInvestigation, required) {
|
||||||
|
t.Fatalf("storage investigation tools missing %q: %v", required, storageInvestigation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatrolScopedToolProjectionFailsOpenToGovernedProfileForUnknownType(t *testing.T) {
|
||||||
|
if names, ok := PatrolEvidenceToolNamesForResourceTypes(nil); ok || names != nil {
|
||||||
|
t.Fatalf("empty scope = (%v, %v), want no projection", names, ok)
|
||||||
|
}
|
||||||
|
if names, ok := PatrolEvidenceToolNamesForResourceTypes([]string{"future-resource-kind"}); ok || names != nil {
|
||||||
|
t.Fatalf("unknown scope = (%v, %v), want no projection", names, ok)
|
||||||
|
}
|
||||||
|
if names, ok := PatrolEvidenceToolNamesForResourceTypes([]string{"app-container", "future-resource-kind"}); ok || names != nil {
|
||||||
|
t.Fatalf("mixed unknown scope = (%v, %v), want no partial projection", names, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatrolScopedToolProjectionComposesProfileOwnedTools(t *testing.T) {
|
||||||
|
detection, ok := PatrolDetectionToolNamesForResourceTypes([]string{"network-endpoint"})
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("network-endpoint scope was not recognized")
|
||||||
|
}
|
||||||
|
for _, required := range []string{PulseQueryToolName, PulseReadToolName, PulseDiscoveryToolName, PatrolGetFindingsToolName, PatrolReportFindingToolName, PatrolAssessFindingToolName, PatrolResolveFindingToolName} {
|
||||||
|
if !slices.Contains(detection, required) {
|
||||||
|
t.Fatalf("detection surface missing %q: %v", required, detection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slices.Contains(detection, PatrolProposeActionToolName) {
|
||||||
|
t.Fatalf("detection surface gained investigation proposal authority: %v", detection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,58 @@
|
||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestRestrictInvestigationProviderToolsForResourceType(t *testing.T) {
|
||||||
|
providerTools := []providers.Tool{
|
||||||
|
{Name: agentcapabilities.PulseQueryToolName},
|
||||||
|
{Name: agentcapabilities.PulseMetricsToolName},
|
||||||
|
{Name: agentcapabilities.PulseStorageToolName},
|
||||||
|
{Name: agentcapabilities.PulseDockerToolName},
|
||||||
|
{Name: agentcapabilities.PulseKubernetesToolName},
|
||||||
|
{Name: agentcapabilities.PulseReadToolName},
|
||||||
|
{Name: agentcapabilities.PulseKnowledgeToolName},
|
||||||
|
{Name: agentcapabilities.PatrolActionCapabilitiesToolName},
|
||||||
|
{Name: agentcapabilities.PatrolProposeActionToolName},
|
||||||
|
}
|
||||||
|
filtered, err := restrictInvestigationProviderToolsForResourceType(providerTools, "app-container")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("restrict Docker investigation: %v", err)
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
for _, tool := range filtered {
|
||||||
|
names = append(names, tool.Name)
|
||||||
|
}
|
||||||
|
want := []string{
|
||||||
|
agentcapabilities.PulseQueryToolName,
|
||||||
|
agentcapabilities.PulseDockerToolName,
|
||||||
|
agentcapabilities.PulseReadToolName,
|
||||||
|
agentcapabilities.PatrolActionCapabilitiesToolName,
|
||||||
|
agentcapabilities.PatrolProposeActionToolName,
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(names, want) {
|
||||||
|
t.Fatalf("Docker investigation tools = %v, want %v", names, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
unchanged, err := restrictInvestigationProviderToolsForResourceType(providerTools, "future-resource-kind")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unknown resource type: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(unchanged, providerTools) {
|
||||||
|
t.Fatalf("unknown resource type changed governed profile: %v", unchanged)
|
||||||
|
}
|
||||||
|
unchanged[0].Name = "mutated"
|
||||||
|
if providerTools[0].Name == "mutated" {
|
||||||
|
t.Fatal("unknown-type fallback aliased the provider manifest")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFilterToolsForPatrol_ConfigFlags(t *testing.T) {
|
func TestFilterToolsForPatrol_ConfigFlags(t *testing.T) {
|
||||||
svc := &Service{
|
svc := &Service{
|
||||||
cfg: &config.AIConfig{
|
cfg: &config.AIConfig{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai/modelboundary"
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/modelboundary"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai/tools"
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/tools"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
@ -25,6 +27,9 @@ type InvestigationRunRequest struct {
|
||||||
MaxTurns int
|
MaxTurns int
|
||||||
MaxEvidenceCalls int
|
MaxEvidenceCalls int
|
||||||
ExecutionID string
|
ExecutionID string
|
||||||
|
// ResourceType is trusted finding metadata used only for least-manifest
|
||||||
|
// projection. Unknown types retain the normal governed profile.
|
||||||
|
ResourceType string
|
||||||
// Identity is the trusted correlation identity injected into any
|
// Identity is the trusted correlation identity injected into any
|
||||||
// captured proposal.
|
// captured proposal.
|
||||||
Identity tools.ProposalIdentity
|
Identity tools.ProposalIdentity
|
||||||
|
|
@ -201,7 +206,12 @@ func (s *Service) ExecuteInvestigationStream(ctx context.Context, req Investigat
|
||||||
// Investigation runs are stateless like Patrol detection: only this
|
// Investigation runs are stateless like Patrol detection: only this
|
||||||
// run's prompt is loaded; the session is a forensic log.
|
// run's prompt is loaded; the session is a forensic log.
|
||||||
messages := []Message{userMsg}
|
messages := []Message{userMsg}
|
||||||
filteredTools := s.toolsForExecutor(executor, false)
|
filteredTools, err := restrictInvestigationProviderToolsForResourceType(
|
||||||
|
s.toolsForExecutor(executor, false), req.ResourceType,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
resultMessages, runErr := loop.ExecuteWithTools(ctx, session.ID, messages, filteredTools, callback)
|
resultMessages, runErr := loop.ExecuteWithTools(ctx, session.ID, messages, filteredTools, callback)
|
||||||
for _, msg := range resultMessages {
|
for _, msg := range resultMessages {
|
||||||
|
|
@ -241,6 +251,18 @@ func (s *Service) ExecuteInvestigationStream(ctx context.Context, req Investigat
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restrictInvestigationProviderToolsForResourceType(providerTools []providers.Tool, resourceType string) ([]providers.Tool, error) {
|
||||||
|
scopedNames, ok := agentcapabilities.PatrolInvestigationToolNamesForResourceTypes([]string{resourceType})
|
||||||
|
if !ok {
|
||||||
|
return append([]providers.Tool(nil), providerTools...), nil
|
||||||
|
}
|
||||||
|
filtered, err := restrictPatrolProviderTools(providerTools, scopedNames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to project scoped investigation tools: %w", err)
|
||||||
|
}
|
||||||
|
return filtered, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListInvestigationTools names the tools an investigation run offers,
|
// ListInvestigationTools names the tools an investigation run offers,
|
||||||
// projected through the same investigation-profile path the run uses.
|
// projected through the same investigation-profile path the run uses.
|
||||||
func (s *Service) ListInvestigationTools() []string {
|
func (s *Service) ListInvestigationTools() []string {
|
||||||
|
|
|
||||||
|
|
@ -1748,12 +1748,21 @@ Do not investigate, report, resolve, or assess any other Patrol finding. Finding
|
||||||
}
|
}
|
||||||
|
|
||||||
func patrolAllowedToolNamesForScope(scope *PatrolScope) []string {
|
func patrolAllowedToolNamesForScope(scope *PatrolScope) []string {
|
||||||
if !isPatrolObjectivePlanningScope(scope) {
|
if isPatrolObjectivePlanningScope(scope) {
|
||||||
|
return []string{
|
||||||
|
agentcapabilities.PatrolProposeObserverToolName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scope == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return []string{
|
resourceTypes := append([]string(nil), scope.ResourceTypes...)
|
||||||
agentcapabilities.PatrolProposeObserverToolName,
|
resourceTypes = append(resourceTypes, scope.resolvedResourceTypes...)
|
||||||
|
allowed, ok := agentcapabilities.PatrolDetectionToolNamesForResourceTypes(resourceTypes)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
return allowed
|
||||||
}
|
}
|
||||||
|
|
||||||
// seedIntelligence holds pre-computed intelligence data used by multiple seed context sections.
|
// seedIntelligence holds pre-computed intelligence data used by multiple seed context sections.
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,35 @@ func TestPatrolObjectivePlanningScopeHasOneMissionAndLeastAuthority(t *testing.T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScopedPatrolUsesCoreResolvedResourceTypeForLeastManifest(t *testing.T) {
|
||||||
|
scope := &PatrolScope{
|
||||||
|
Reason: TriggerReasonManual,
|
||||||
|
ResourceIDs: []string{"container-1"},
|
||||||
|
resolvedResourceTypes: []string{"app-container"},
|
||||||
|
}
|
||||||
|
allowed := patrolAllowedToolNamesForScope(scope)
|
||||||
|
for _, required := range []string{
|
||||||
|
agentcapabilities.PulseQueryToolName,
|
||||||
|
agentcapabilities.PulseReadToolName,
|
||||||
|
agentcapabilities.PulseDockerToolName,
|
||||||
|
agentcapabilities.PatrolReportFindingToolName,
|
||||||
|
} {
|
||||||
|
if !slices.Contains(allowed, required) {
|
||||||
|
t.Fatalf("scoped detection tools missing %q: %v", required, allowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, forbidden := range []string{
|
||||||
|
agentcapabilities.PulseStorageToolName,
|
||||||
|
agentcapabilities.PulseKubernetesToolName,
|
||||||
|
agentcapabilities.PulsePMGToolName,
|
||||||
|
agentcapabilities.PatrolProposeActionToolName,
|
||||||
|
} {
|
||||||
|
if slices.Contains(allowed, forbidden) {
|
||||||
|
t.Fatalf("scoped detection tools included %q: %v", forbidden, allowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPatrolObjectivePlanningSeedExcludesGlobalFindingLifecycle(t *testing.T) {
|
func TestPatrolObjectivePlanningSeedExcludesGlobalFindingLifecycle(t *testing.T) {
|
||||||
patrol := NewPatrolService(nil, nil)
|
patrol := NewPatrolService(nil, nil)
|
||||||
patrol.findings.Add(&Finding{
|
patrol.findings.Add(&Finding{
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ func TestResolvePatrolScopeExpandsCanonicalUnifiedIDToSourceResource(t *testing.
|
||||||
if len(resolution.EffectiveResourceIDs) == 0 {
|
if len(resolution.EffectiveResourceIDs) == 0 {
|
||||||
t.Fatalf("expected effective resource IDs, got %+v", resolution)
|
t.Fatalf("expected effective resource IDs, got %+v", resolution)
|
||||||
}
|
}
|
||||||
|
if len(resolved.resolvedResourceTypes) != 1 || resolved.resolvedResourceTypes[0] != "vm" {
|
||||||
|
t.Fatalf("resolved resource types = %v, want [vm]", resolved.resolvedResourceTypes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolvePatrolScopePreservesCanonicalAvailabilityEndpoint(t *testing.T) {
|
func TestResolvePatrolScopePreservesCanonicalAvailabilityEndpoint(t *testing.T) {
|
||||||
|
|
@ -108,6 +111,9 @@ func TestResolvePatrolScopePreservesCanonicalAvailabilityEndpoint(t *testing.T)
|
||||||
if counts.unified != 1 || counts.total() != 1 {
|
if counts.unified != 1 || counts.total() != 1 {
|
||||||
t.Fatalf("canonical endpoint counts = %#v, want one unified resource", counts)
|
t.Fatalf("canonical endpoint counts = %#v, want one unified resource", counts)
|
||||||
}
|
}
|
||||||
|
if len(resolved.resolvedResourceTypes) != 1 || resolved.resolvedResourceTypes[0] != "network-endpoint" {
|
||||||
|
t.Fatalf("resolved endpoint types = %v, want [network-endpoint]", resolved.resolvedResourceTypes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolvePatrolScopeRejectsUnmatchedAndAmbiguousAliases(t *testing.T) {
|
func TestResolvePatrolScopeRejectsUnmatchedAndAmbiguousAliases(t *testing.T) {
|
||||||
|
|
@ -156,13 +162,19 @@ func TestResolvePatrolScopeResolvesDockerAlertResourceIDs(t *testing.T) {
|
||||||
readState := newPatrolRuntimeStateWithProviders(snapshot, registry, unifiedresources.NewUnifiedAIAdapter(registry))
|
readState := newPatrolRuntimeStateWithProviders(snapshot, registry, unifiedresources.NewUnifiedAIAdapter(registry))
|
||||||
|
|
||||||
for _, state := range []patrolRuntimeState{snapshotState, readState} {
|
for _, state := range []patrolRuntimeState{snapshotState, readState} {
|
||||||
for _, requested := range []string{containerAlertID, hostAlertID} {
|
for _, tc := range []struct {
|
||||||
resolved, resolution := resolvePatrolScopeState(state, PatrolScope{ResourceIDs: []string{requested}})
|
requested string
|
||||||
|
resourceType string
|
||||||
|
}{{containerAlertID, "app-container"}, {hostAlertID, "docker-host"}} {
|
||||||
|
resolved, resolution := resolvePatrolScopeState(state, PatrolScope{ResourceIDs: []string{tc.requested}})
|
||||||
if len(resolution.UnmatchedResourceIDs) != 0 || len(resolution.AmbiguousResourceIDs) != 0 {
|
if len(resolution.UnmatchedResourceIDs) != 0 || len(resolution.AmbiguousResourceIDs) != 0 {
|
||||||
t.Fatalf("alert resource ID %q did not resolve exactly: %+v", requested, resolution)
|
t.Fatalf("alert resource ID %q did not resolve exactly: %+v", tc.requested, resolution)
|
||||||
}
|
}
|
||||||
if len(resolved.ResourceIDs) == 0 {
|
if len(resolved.ResourceIDs) == 0 {
|
||||||
t.Fatalf("alert resource ID %q resolved to an empty scope", requested)
|
t.Fatalf("alert resource ID %q resolved to an empty scope", tc.requested)
|
||||||
|
}
|
||||||
|
if len(resolved.resolvedResourceTypes) != 1 || resolved.resolvedResourceTypes[0] != tc.resourceType {
|
||||||
|
t.Fatalf("alert resource ID %q resolved types = %v, want [%s]", tc.requested, resolved.resolvedResourceTypes, tc.resourceType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,10 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type patrolRuntimeResourceRecord struct {
|
type patrolRuntimeResourceRecord struct {
|
||||||
kind patrolRuntimeResourceKind
|
kind patrolRuntimeResourceKind
|
||||||
ids []string
|
resourceType string
|
||||||
aliases []string
|
ids []string
|
||||||
|
aliases []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// patrolRuntimeState is the patrol subsystem's internal runtime state contract.
|
// patrolRuntimeState is the patrol subsystem's internal runtime state contract.
|
||||||
|
|
@ -181,8 +182,11 @@ func (p *PatrolService) patrolRuntimeStateForSnapshot(snapshot models.StateSnaps
|
||||||
|
|
||||||
func patrolVisitRuntimeResources(s patrolRuntimeState, visit func(patrolRuntimeResourceRecord) bool) {
|
func patrolVisitRuntimeResources(s patrolRuntimeState, visit func(patrolRuntimeResourceRecord) bool) {
|
||||||
emittedIDs := make(map[string]struct{})
|
emittedIDs := make(map[string]struct{})
|
||||||
emit := func(kind patrolRuntimeResourceKind, ids []string, aliases ...string) bool {
|
emitTyped := func(kind patrolRuntimeResourceKind, resourceType string, ids []string, aliases ...string) bool {
|
||||||
if !visit(patrolRuntimeResourceRecord{kind: kind, ids: ids, aliases: aliases}) {
|
if resourceType == "" {
|
||||||
|
resourceType = canonicalPatrolRuntimeResourceType(kind)
|
||||||
|
}
|
||||||
|
if !visit(patrolRuntimeResourceRecord{kind: kind, resourceType: resourceType, ids: ids, aliases: aliases}) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
|
|
@ -192,6 +196,9 @@ func patrolVisitRuntimeResources(s patrolRuntimeState, visit func(patrolRuntimeR
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
emit := func(kind patrolRuntimeResourceKind, ids []string, aliases ...string) bool {
|
||||||
|
return emitTyped(kind, "", ids, aliases...)
|
||||||
|
}
|
||||||
hostResourceKind := func(platform string) patrolRuntimeResourceKind {
|
hostResourceKind := func(platform string) patrolRuntimeResourceKind {
|
||||||
if strings.EqualFold(strings.TrimSpace(platform), "truenas") {
|
if strings.EqualFold(strings.TrimSpace(platform), "truenas") {
|
||||||
return patrolRuntimeResourceTrueNAS
|
return patrolRuntimeResourceTrueNAS
|
||||||
|
|
@ -322,7 +329,7 @@ func patrolVisitRuntimeResources(s patrolRuntimeState, visit func(patrolRuntimeR
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ids, aliases := patrolUnifiedResourceIdentity(resource)
|
ids, aliases := patrolUnifiedResourceIdentity(resource)
|
||||||
if !emit(patrolRuntimeResourceUnified, ids, aliases...) {
|
if !emitTyped(patrolRuntimeResourceUnified, string(resource.Type), ids, aliases...) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -466,10 +473,33 @@ func canonicalPatrolScopeToken(value string) string {
|
||||||
return strings.ToLower(strings.TrimSpace(value))
|
return strings.ToLower(strings.TrimSpace(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func canonicalPatrolRuntimeResourceType(kind patrolRuntimeResourceKind) string {
|
||||||
|
switch kind {
|
||||||
|
case patrolRuntimeResourceContainer:
|
||||||
|
return "system-container"
|
||||||
|
case patrolRuntimeResourceDockerItem:
|
||||||
|
return "app-container"
|
||||||
|
case patrolRuntimeResourcePhysicalDisk:
|
||||||
|
return "physical-disk"
|
||||||
|
case patrolRuntimeResourceHost:
|
||||||
|
return "agent"
|
||||||
|
case patrolRuntimeResourceTrueNAS:
|
||||||
|
return "truenas"
|
||||||
|
case patrolRuntimeResourceUnified:
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return string(kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// resolvePatrolScopeState expands canonical unified IDs, source IDs, and
|
// resolvePatrolScopeState expands canonical unified IDs, source IDs, and
|
||||||
// unique known aliases onto the complete identity set for the matching Patrol
|
// unique known aliases onto the complete identity set for the matching Patrol
|
||||||
// runtime record. It never fuzzy-matches and refuses ambiguous aliases.
|
// runtime record. It never fuzzy-matches and refuses ambiguous aliases.
|
||||||
func resolvePatrolScopeState(state patrolRuntimeState, scope PatrolScope) (PatrolScope, PatrolScopeResolution) {
|
func resolvePatrolScopeState(state patrolRuntimeState, scope PatrolScope) (PatrolScope, PatrolScopeResolution) {
|
||||||
|
// Recompute private type evidence from the current runtime snapshot on every
|
||||||
|
// resolution pass; callers may synchronously pre-resolve and the accepted run
|
||||||
|
// resolves again to close collection races.
|
||||||
|
scope.resolvedResourceTypes = nil
|
||||||
resolution := PatrolScopeResolution{}
|
resolution := PatrolScopeResolution{}
|
||||||
for _, requested := range scope.ResourceIDs {
|
for _, requested := range scope.ResourceIDs {
|
||||||
if requested = strings.TrimSpace(requested); requested != "" {
|
if requested = strings.TrimSpace(requested); requested != "" {
|
||||||
|
|
@ -483,16 +513,18 @@ func resolvePatrolScopeState(state patrolRuntimeState, scope PatrolScope) (Patro
|
||||||
}
|
}
|
||||||
|
|
||||||
type identityRecord struct {
|
type identityRecord struct {
|
||||||
ids []string
|
ids []string
|
||||||
aliases []string
|
aliases []string
|
||||||
idTokens map[string]bool
|
resourceType string
|
||||||
aliasTokens map[string]bool
|
idTokens map[string]bool
|
||||||
|
aliasTokens map[string]bool
|
||||||
}
|
}
|
||||||
records := make([]identityRecord, 0)
|
records := make([]identityRecord, 0)
|
||||||
patrolVisitRuntimeResources(state, func(record patrolRuntimeResourceRecord) bool {
|
patrolVisitRuntimeResources(state, func(record patrolRuntimeResourceRecord) bool {
|
||||||
candidate := identityRecord{
|
candidate := identityRecord{
|
||||||
ids: append([]string(nil), record.ids...), aliases: append([]string(nil), record.aliases...),
|
ids: append([]string(nil), record.ids...), aliases: append([]string(nil), record.aliases...),
|
||||||
idTokens: make(map[string]bool), aliasTokens: make(map[string]bool),
|
resourceType: record.resourceType,
|
||||||
|
idTokens: make(map[string]bool), aliasTokens: make(map[string]bool),
|
||||||
}
|
}
|
||||||
for _, value := range record.ids {
|
for _, value := range record.ids {
|
||||||
if token := canonicalPatrolScopeToken(value); token != "" {
|
if token := canonicalPatrolScopeToken(value); token != "" {
|
||||||
|
|
@ -513,6 +545,7 @@ func resolvePatrolScopeState(state patrolRuntimeState, scope PatrolScope) (Patro
|
||||||
expanded := make([]string, 0, len(scope.ResourceIDs))
|
expanded := make([]string, 0, len(scope.ResourceIDs))
|
||||||
seenExpanded := make(map[string]bool)
|
seenExpanded := make(map[string]bool)
|
||||||
seenResolved := make(map[string]bool)
|
seenResolved := make(map[string]bool)
|
||||||
|
seenResolvedTypes := make(map[string]bool)
|
||||||
addExpanded := func(value string) {
|
addExpanded := func(value string) {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
if value != "" && !seenExpanded[value] {
|
if value != "" && !seenExpanded[value] {
|
||||||
|
|
@ -527,6 +560,13 @@ func resolvePatrolScopeState(state patrolRuntimeState, scope PatrolScope) (Patro
|
||||||
resolution.ResolvedResourceIDs = append(resolution.ResolvedResourceIDs, value)
|
resolution.ResolvedResourceIDs = append(resolution.ResolvedResourceIDs, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
addResolvedType := func(value string) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value != "" && !seenResolvedTypes[value] {
|
||||||
|
seenResolvedTypes[value] = true
|
||||||
|
scope.resolvedResourceTypes = append(scope.resolvedResourceTypes, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, requested := range resolution.RequestedResourceIDs {
|
for _, requested := range resolution.RequestedResourceIDs {
|
||||||
token := canonicalPatrolScopeToken(requested)
|
token := canonicalPatrolScopeToken(requested)
|
||||||
matches := make([]identityRecord, 0, 1)
|
matches := make([]identityRecord, 0, 1)
|
||||||
|
|
@ -552,6 +592,7 @@ func resolvePatrolScopeState(state patrolRuntimeState, scope PatrolScope) (Patro
|
||||||
addExpanded(id)
|
addExpanded(id)
|
||||||
addResolved(id)
|
addResolved(id)
|
||||||
}
|
}
|
||||||
|
addResolvedType(matches[0].resourceType)
|
||||||
default:
|
default:
|
||||||
resolution.AmbiguousResourceIDs = append(resolution.AmbiguousResourceIDs, requested)
|
resolution.AmbiguousResourceIDs = append(resolution.AmbiguousResourceIDs, requested)
|
||||||
}
|
}
|
||||||
|
|
@ -563,6 +604,7 @@ func resolvePatrolScopeState(state patrolRuntimeState, scope PatrolScope) (Patro
|
||||||
resolution.EffectiveResourceIDs = patrolRuntimeSortedResourceIDs(filtered)
|
resolution.EffectiveResourceIDs = patrolRuntimeSortedResourceIDs(filtered)
|
||||||
}
|
}
|
||||||
sort.Strings(resolution.ResolvedResourceIDs)
|
sort.Strings(resolution.ResolvedResourceIDs)
|
||||||
|
sort.Strings(scope.resolvedResourceTypes)
|
||||||
sort.Strings(resolution.EffectiveResourceIDs)
|
sort.Strings(resolution.EffectiveResourceIDs)
|
||||||
sort.Strings(resolution.UnmatchedResourceIDs)
|
sort.Strings(resolution.UnmatchedResourceIDs)
|
||||||
sort.Strings(resolution.AmbiguousResourceIDs)
|
sort.Strings(resolution.AmbiguousResourceIDs)
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,10 @@ type PatrolScope struct {
|
||||||
// prevents an authoritative ID from subsequently matching a different
|
// prevents an authoritative ID from subsequently matching a different
|
||||||
// resource whose display alias happens to contain the same string.
|
// resource whose display alias happens to contain the same string.
|
||||||
resolvedIdentityOnly bool
|
resolvedIdentityOnly bool
|
||||||
|
// resolvedResourceTypes is the core-derived type set for exact resource-ID
|
||||||
|
// scopes. It is used only to reduce the provider tool manifest; it never
|
||||||
|
// broadens collection scope or mutation authority.
|
||||||
|
resolvedResourceTypes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatrolScopeResolution records how caller-supplied identities map onto the
|
// PatrolScopeResolution records how caller-supplied identities map onto the
|
||||||
|
|
|
||||||
|
|
@ -2199,6 +2199,7 @@ func (a *orchestratorChatAdapter) ExecuteInvestigationStream(ctx context.Context
|
||||||
MaxTurns: req.MaxTurns,
|
MaxTurns: req.MaxTurns,
|
||||||
MaxEvidenceCalls: req.MaxEvidenceCalls,
|
MaxEvidenceCalls: req.MaxEvidenceCalls,
|
||||||
ExecutionID: req.ExecutionID,
|
ExecutionID: req.ExecutionID,
|
||||||
|
ResourceType: req.ResourceType,
|
||||||
Identity: tools.ProposalIdentity{
|
Identity: tools.ProposalIdentity{
|
||||||
ProposalID: req.ProposalID,
|
ProposalID: req.ProposalID,
|
||||||
FindingID: req.FindingID,
|
FindingID: req.FindingID,
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,14 @@ func TestAIHandlersUseSafeRemediationCommercialCopy(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOrchestratorChatAdapterCarriesTypedInvestigationScope(t *testing.T) {
|
||||||
|
source, err := os.ReadFile("ai_handlers.go")
|
||||||
|
require.NoError(t, err)
|
||||||
|
text := string(source)
|
||||||
|
require.Contains(t, text, "ResourceType: req.ResourceType")
|
||||||
|
require.NotContains(t, text, "ResourceType: req.Prompt")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAISettingsHandler_AnthropicOAuthSetupFailsClosed(t *testing.T) {
|
func TestAISettingsHandler_AnthropicOAuthSetupFailsClosed(t *testing.T) {
|
||||||
handler := &AISettingsHandler{}
|
handler := &AISettingsHandler{}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1074,6 +1074,7 @@ func TestContract_AssistantFindingContextUsesModelOnlyHandoff(t *testing.T) {
|
||||||
"catalog: h.proposalCatalogFor(orgID)",
|
"catalog: h.proposalCatalogFor(orgID)",
|
||||||
"func (a *orchestratorChatAdapter) ExecuteInvestigationStream(",
|
"func (a *orchestratorChatAdapter) ExecuteInvestigationStream(",
|
||||||
"MaxEvidenceCalls: req.MaxEvidenceCalls",
|
"MaxEvidenceCalls: req.MaxEvidenceCalls",
|
||||||
|
"ResourceType: req.ResourceType",
|
||||||
"ModelTurns: runResult.ModelTurns",
|
"ModelTurns: runResult.ModelTurns",
|
||||||
"EvidenceCalls: runResult.EvidenceCalls",
|
"EvidenceCalls: runResult.EvidenceCalls",
|
||||||
} {
|
} {
|
||||||
|
|
|
||||||
|
|
@ -418,7 +418,7 @@ func TestInvestigationResultCarriesStructuredProposal(t *testing.T) {
|
||||||
|
|
||||||
func TestInvestigationBudgetSeparatesEvidenceCallsFromModelTurns(t *testing.T) {
|
func TestInvestigationBudgetSeparatesEvidenceCallsFromModelTurns(t *testing.T) {
|
||||||
requestType := reflect.TypeOf(OrchestratorInvestigationRequest{})
|
requestType := reflect.TypeOf(OrchestratorInvestigationRequest{})
|
||||||
for _, name := range []string{"MaxTurns", "MaxEvidenceCalls"} {
|
for _, name := range []string{"MaxTurns", "MaxEvidenceCalls", "ResourceType"} {
|
||||||
if _, ok := requestType.FieldByName(name); !ok {
|
if _, ok := requestType.FieldByName(name); !ok {
|
||||||
t.Fatalf("investigation request missing %s", name)
|
t.Fatalf("investigation request missing %s", name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,16 +39,19 @@ type OrchestratorChatService interface {
|
||||||
// identity is injected here by the orchestrator from trusted context; the
|
// identity is injected here by the orchestrator from trusted context; the
|
||||||
// model's proposal tool schema never carries it.
|
// model's proposal tool schema never carries it.
|
||||||
type OrchestratorInvestigationRequest struct {
|
type OrchestratorInvestigationRequest struct {
|
||||||
SessionID string `json:"session_id,omitempty"`
|
SessionID string `json:"session_id,omitempty"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||||
MaxTurns int `json:"max_turns,omitempty"`
|
MaxTurns int `json:"max_turns,omitempty"`
|
||||||
MaxEvidenceCalls int `json:"max_evidence_calls,omitempty"`
|
MaxEvidenceCalls int `json:"max_evidence_calls,omitempty"`
|
||||||
ExecutionID string `json:"execution_id,omitempty"`
|
ExecutionID string `json:"execution_id,omitempty"`
|
||||||
ProposalID string `json:"proposal_id"`
|
ProposalID string `json:"proposal_id"`
|
||||||
FindingID string `json:"finding_id"`
|
FindingID string `json:"finding_id"`
|
||||||
InvestigationID string `json:"investigation_id"`
|
InvestigationID string `json:"investigation_id"`
|
||||||
EvidenceIDs []string `json:"evidence_ids,omitempty"`
|
// ResourceType is trusted finding metadata used by core only to reduce the
|
||||||
|
// investigation tool manifest. It cannot add tools or mutation authority.
|
||||||
|
ResourceType string `json:"resource_type,omitempty"`
|
||||||
|
EvidenceIDs []string `json:"evidence_ids,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrchestratorInvestigationResult is the structured outcome of one
|
// OrchestratorInvestigationResult is the structured outcome of one
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue