Clarify Patrol scope identities and finding reads

This commit is contained in:
rcourtman
2026-07-14 19:23:36 +01:00
parent e008343b7a
commit 9ea8cb2aa1
7 changed files with 34 additions and 17 deletions
@@ -5705,6 +5705,12 @@ read-only host response when the identity resolves, and returns a successful
typed `not_found` result when it does not; a model following the advertised
schema must not incur a failed tool call because the executor used a narrower
legacy type switch.
Scoped Patrol prompt context must describe canonical/source IDs and aliases as
identity aliases, not count each value as a distinct infrastructure resource.
The exact scoped inventory is the authoritative resource set. Patrol must call
`patrol_get_findings` once near the beginning of a run and reuse that snapshot
for all lifecycle decisions, avoiding duplicate reads and their extra provider
turns on both healthy and faulted runs.
Persisted Patrol tool inputs must retain complete structured finding calls up
to the bounded 16 KiB record limit so normal evidence-rich findings remain
deterministically replayable. If any captured input is nevertheless incomplete
+10 -9
View File
@@ -1201,13 +1201,13 @@ The seed context includes service identity (from discovery) and reachability dat
A direct provider-reported failed health check, failed backup, or broken replication state is already confirmed evidence of an operational symptom. Report that symptom even when logs or command execution are unavailable. Use warning/reliability for a failed health check unless the evidence establishes a critical consequence. State that the root cause is unknown and recommend the next safe diagnostic step; never invent a root cause. Missing optional root-cause evidence must not suppress a confirmed symptom-level finding.
**Step 3 — Report or assess findings.** Report new confirmed issues with patrol_report_finding. For every active finding returned by patrol_get_findings, call patrol_assess_finding exactly once with present, resolved, or uncertain and current evidence. Do not silently skip a known finding: omission is not evidence that it cleared. patrol_resolve_finding remains available for compatibility, but patrol_assess_finding is the complete existing-finding verdict.
Always call patrol_get_findings before reporting, assessing, or resolving findings.
**Step 3 — Report or assess findings.** Report new confirmed issues with patrol_report_finding. Call patrol_get_findings exactly once near the beginning of the run and reuse that result; do not call it again before the final summary. For every active finding it returned, call patrol_assess_finding exactly once with present, resolved, or uncertain and current evidence. Do not silently skip a known finding: omission is not evidence that it cleared. patrol_resolve_finding remains available for compatibility, but patrol_assess_finding is the complete existing-finding verdict.
The snapshot eliminates routine data gathering. When a notable signal needs current or historical confirmation, gather enough evidence to distinguish real problems from noise before reporting it.
## Efficiency Rules
- Do NOT call the same tool with the same parameters twice in a single patrol run.
- In particular, call patrol_get_findings once and reuse its result for all finding lifecycle decisions in the run.
- Keep track of what you've already checked. If you've already retrieved metrics for a resource, use the data you have.
- Once direct resource evidence confirms an actionable symptom, report it before pursuing optional root-cause detail.
- If a tool reports that a resource lacks the required agent or native capability, do not retry that capability or replace it with a broad inventory scan. Continue with the evidence already available.
@@ -1642,7 +1642,7 @@ func (p *PatrolService) assembleSeedWithinBudget(sections []seedSection, budgetT
return sb.String()
}
func buildScopeSection(scope *PatrolScope, effectiveScopeIDs []string) string {
func buildScopeSection(scope *PatrolScope, effectiveIdentityAliases []string) string {
if scope == nil {
return ""
}
@@ -1656,16 +1656,17 @@ func buildScopeSection(scope *PatrolScope, effectiveScopeIDs []string) string {
sb.WriteString(fmt.Sprintf("Context: %s\n", scope.Context))
}
if len(scope.ResourceIDs) > 0 {
sb.WriteString(fmt.Sprintf("Requested resources: %s\n", strings.Join(scope.ResourceIDs, ", ")))
sb.WriteString(fmt.Sprintf("Resolved requested identity aliases: %s\n", strings.Join(scope.ResourceIDs, ", ")))
}
if len(scope.ResourceTypes) > 0 {
sb.WriteString(fmt.Sprintf("Requested resource types: %s\n", strings.Join(scope.ResourceTypes, ", ")))
}
if len(effectiveScopeIDs) > 0 {
sb.WriteString(fmt.Sprintf("Effective scope: %d %s (%s)\n",
len(effectiveScopeIDs),
seedCountLabel(len(effectiveScopeIDs), "resource", "resources"),
seedTruncateOutlierList(effectiveScopeIDs, 8)))
if len(effectiveIdentityAliases) > 0 {
sb.WriteString(fmt.Sprintf("Model-context identity aliases: %d %s (%s)\n",
len(effectiveIdentityAliases),
seedCountLabel(len(effectiveIdentityAliases), "alias", "aliases"),
seedTruncateOutlierList(effectiveIdentityAliases, 8)))
sb.WriteString("Identity aliases are not separate infrastructure resources. Multiple aliases can identify the same scoped resource; do not query each alias. Use the exact scoped inventory rows below as the authoritative resource set.\n")
}
if scope.AlertIdentifier != "" {
sb.WriteString(fmt.Sprintf("Alert Identifier: %s\n", scope.AlertIdentifier))
+1
View File
@@ -327,6 +327,7 @@ func TestGetPatrolSystemPrompt_IncludesTrustScaffoldingGuidance(t *testing.T) {
"Authoring Impact",
"Authoring Evidence",
"trust anchor",
"Call patrol_get_findings exactly once",
}
for _, want := range required {
if !strings.Contains(prompt, want) {
@@ -57,14 +57,17 @@ func TestPatrolService_buildSeedContext_ScopeSection(t *testing.T) {
if !strings.Contains(seed, "Context: CPU alert") {
t.Fatalf("expected context in scope section, got:\n%s", seed)
}
if !strings.Contains(seed, "Requested resources: node-1") {
t.Fatalf("expected resource IDs in scope section, got:\n%s", seed)
if !strings.Contains(seed, "Resolved requested identity aliases: node-1") {
t.Fatalf("expected resolved identity aliases in scope section, got:\n%s", seed)
}
if !strings.Contains(seed, "Requested resource types: node") {
t.Fatalf("expected resource types in scope section, got:\n%s", seed)
}
if !strings.Contains(seed, "Effective scope: 1 resource (node-1)") {
t.Fatalf("expected effective scope in scope section, got:\n%s", seed)
if !strings.Contains(seed, "Model-context identity aliases: 1 alias (node-1)") {
t.Fatalf("expected model-context aliases in scope section, got:\n%s", seed)
}
if !strings.Contains(seed, "Identity aliases are not separate infrastructure resources") {
t.Fatalf("expected identity/resource boundary in scope section, got:\n%s", seed)
}
if !strings.Contains(seed, "Alert Identifier: alert-123") {
t.Fatalf("expected alert identifier in scope section, got:\n%s", seed)
@@ -92,7 +95,7 @@ func TestPatrolService_buildSeedContext_TypeScopedEffectiveScopeSection(t *testi
}
seed, _ := ps.buildSeedContextState(patrolRuntimeStateForTest(ps, state), scope, nil)
if !strings.Contains(seed, "Effective scope: 2 resources (node-1, node-2)") {
t.Fatalf("expected type-scoped effective scope section, got:\n%s", seed)
if !strings.Contains(seed, "Model-context identity aliases: 2 aliases (node-1, node-2)") {
t.Fatalf("expected type-scoped model-context aliases, got:\n%s", seed)
}
}
@@ -106,7 +106,7 @@ func TestBuildTriageSeedContext_UnionsExplicitScopeWithFlags(t *testing.T) {
t.Fatalf("did not expect unrelated resource %q in triage seed, got:\n%s", unexpected, seed)
}
}
if !strings.Contains(seed, "Effective scope: 2 resources (qemu/102, qemu/107)") {
if !strings.Contains(seed, "Model-context identity aliases: 2 aliases (qemu/102, qemu/107)") {
t.Fatalf("expected scope section to describe the complete evidence set, got:\n%s", seed)
}
}
+1 -1
View File
@@ -169,7 +169,7 @@ Returns: {"ok": true, "resolved": true} on success.`,
e.registry.registerBuiltin(RegisteredTool{
Definition: Tool{
Name: agentcapabilities.PatrolGetFindingsToolName,
Description: `Get currently active patrol findings. Use this to check what findings already exist before reporting new ones (avoids duplicates) and to identify findings that may need resolution.
Description: `Get currently active patrol findings. Call this exactly once near the beginning of a Patrol run, then reuse the result to check what findings already exist before reporting new ones (avoids duplicates) and to identify findings that may need resolution. Do not call it again before the final summary.
Returns a list of active findings with their IDs, severity, resource, and title.`,
InputSchema: InputSchema{
+6
View File
@@ -758,6 +758,7 @@ func TestPatrolToolsRegistered(t *testing.T) {
found := map[string]bool{}
var resolveTool Tool
var reportTool Tool
var getFindingsTool Tool
for _, tool := range tools {
if tool.Name == "patrol_report_finding" || tool.Name == "patrol_assess_finding" || tool.Name == "patrol_resolve_finding" || tool.Name == "patrol_get_findings" {
found[tool.Name] = true
@@ -768,6 +769,9 @@ func TestPatrolToolsRegistered(t *testing.T) {
if tool.Name == "patrol_report_finding" {
reportTool = tool
}
if tool.Name == "patrol_get_findings" {
getFindingsTool = tool
}
}
assert.True(t, found["patrol_report_finding"], "patrol_report_finding should be registered")
@@ -782,6 +786,8 @@ func TestPatrolToolsRegistered(t *testing.T) {
require.NotEmpty(t, reportTool.Name)
assert.Contains(t, reportTool.Description, "failed health check")
assert.Contains(t, reportTool.Description, "root cause is unknown")
require.NotEmpty(t, getFindingsTool.Name)
assert.Contains(t, getFindingsTool.Description, "exactly once")
}
func TestPatrolToolsAvailability(t *testing.T) {