Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions core/aggregate/agentdeploy/correlate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package agentdeploy

import (
"sort"
"strings"

agginventory "github.com/Clyra-AI/wrkr/core/aggregate/inventory"
"github.com/Clyra-AI/wrkr/core/identity"
"github.com/Clyra-AI/wrkr/core/model"
)

type deploymentSignals struct {
artifacts map[string]struct{}
evidence map[string]struct{}
}

func Resolve(findings []model.Finding) map[string]agginventory.AgentDeploymentContext {
resolved := map[string]deploymentSignals{}
for _, finding := range findings {
if !model.IsInventoryBearingFinding(finding) {
continue
}
instanceID := agentInstanceID(finding)
entry := resolved[instanceID]
if entry.artifacts == nil {
entry.artifacts = map[string]struct{}{}
entry.evidence = map[string]struct{}{}
}

for _, artifact := range inferredArtifacts(finding) {
entry.artifacts[artifact] = struct{}{}
entry.evidence["deployment:"+artifact] = struct{}{}
}
resolved[instanceID] = entry
}

out := map[string]agginventory.AgentDeploymentContext{}
for instanceID, entry := range resolved {
artifacts := sortedKeys(entry.artifacts)
status := "unknown"
switch {
case len(artifacts) == 1:
status = "deployed"
case len(artifacts) > 1:
status = "ambiguous"
}
out[instanceID] = agginventory.AgentDeploymentContext{
DeploymentStatus: status,
DeploymentArtifacts: artifacts,
DeploymentEvidenceKeys: sortedKeys(entry.evidence),
}
}
return out
}

func inferredArtifacts(finding model.Finding) []string {
artifacts := map[string]struct{}{}
location := strings.TrimSpace(finding.Location)
if strings.HasPrefix(location, ".github/workflows/") || strings.HasSuffix(location, ".github/workflows") {
artifacts[location] = struct{}{}
}
lowerLocation := strings.ToLower(location)
if strings.Contains(lowerLocation, "dockerfile") || strings.Contains(lowerLocation, "k8s") || strings.Contains(lowerLocation, "helm") {
artifacts[location] = struct{}{}
}
for _, evidence := range finding.Evidence {
key := strings.ToLower(strings.TrimSpace(evidence.Key))
if key != "deployment_artifacts" && key != "deployment_artifact" {
continue
}
for _, item := range splitCSV(evidence.Value) {
artifacts[item] = struct{}{}
}
}
return sortedKeys(artifacts)
}

func agentInstanceID(finding model.Finding) string {
symbol := ""
for _, evidence := range finding.Evidence {
key := strings.ToLower(strings.TrimSpace(evidence.Key))
if key == "symbol" || key == "name" || key == "agent_name" {
symbol = strings.TrimSpace(evidence.Value)
break
}
}
startLine := 0
endLine := 0
if finding.LocationRange != nil {
startLine = finding.LocationRange.StartLine
endLine = finding.LocationRange.EndLine
}
return identity.AgentInstanceID(finding.ToolType, finding.Location, symbol, startLine, endLine)
}

func splitCSV(value string) []string {
parts := strings.Split(value, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
continue
}
out = append(out, trimmed)
}
sort.Strings(out)
return out
}

func sortedKeys(values map[string]struct{}) []string {
if len(values) == 0 {
return nil
}
out := make([]string, 0, len(values))
for item := range values {
out = append(out, item)
}
sort.Strings(out)
return out
}
65 changes: 65 additions & 0 deletions core/aggregate/agentdeploy/correlate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package agentdeploy

import (
"reflect"
"testing"

"github.com/Clyra-AI/wrkr/core/identity"
"github.com/Clyra-AI/wrkr/core/model"
)

func TestAgentDeploymentCorrelator_MatchesArtifactsDeterministically(t *testing.T) {
t.Parallel()

findings := []model.Finding{
{
FindingType: "agent_framework",
ToolType: "langchain",
Location: "agents/release.py",
Evidence: []model.Evidence{
{Key: "symbol", Value: "release_agent"},
{Key: "deployment_artifacts", Value: ".github/workflows/release.yml"},
},
},
}

first := Resolve(findings)
second := Resolve(findings)
if !reflect.DeepEqual(first, second) {
t.Fatalf("expected deterministic correlation output\nfirst=%+v\nsecond=%+v", first, second)
}

instanceID := identity.AgentInstanceID("langchain", "agents/release.py", "release_agent", 0, 0)
deployment := first[instanceID]
if deployment.DeploymentStatus != "deployed" {
t.Fatalf("expected deployed status, got %q", deployment.DeploymentStatus)
}
if !reflect.DeepEqual(deployment.DeploymentArtifacts, []string{".github/workflows/release.yml"}) {
t.Fatalf("unexpected deployment artifacts: %+v", deployment.DeploymentArtifacts)
}
}

func TestAgentDeploymentCorrelator_AmbiguousPathFailClosed(t *testing.T) {
t.Parallel()

findings := []model.Finding{
{
FindingType: "agent_framework",
ToolType: "crewai",
Location: "agents/ops.py",
Evidence: []model.Evidence{
{Key: "symbol", Value: "ops_agent"},
{Key: "deployment_artifacts", Value: "Dockerfile,.github/workflows/deploy.yml"},
},
},
}

instanceID := identity.AgentInstanceID("crewai", "agents/ops.py", "ops_agent", 0, 0)
deployment := Resolve(findings)[instanceID]
if deployment.DeploymentStatus != "ambiguous" {
t.Fatalf("expected ambiguous status, got %q", deployment.DeploymentStatus)
}
if !reflect.DeepEqual(deployment.DeploymentArtifacts, []string{".github/workflows/deploy.yml", "Dockerfile"}) {
t.Fatalf("expected deterministic ambiguous artifacts, got %+v", deployment.DeploymentArtifacts)
}
}
132 changes: 132 additions & 0 deletions core/aggregate/agentresolver/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package agentresolver

import (
"sort"
"strings"

agginventory "github.com/Clyra-AI/wrkr/core/aggregate/inventory"
"github.com/Clyra-AI/wrkr/core/identity"
"github.com/Clyra-AI/wrkr/core/model"
)

type bucket struct {
tools map[string]struct{}
dataSources map[string]struct{}
authSurfaces map[string]struct{}
evidenceKeys map[string]struct{}
}

func Resolve(findings []model.Finding) map[string]agginventory.AgentBindingContext {
resolved := map[string]bucket{}
for _, finding := range findings {
if !model.IsInventoryBearingFinding(finding) {
continue
}
instanceID := agentInstanceID(finding)
if strings.TrimSpace(instanceID) == "" {
continue
}
entry := resolved[instanceID]
if entry.tools == nil {
entry = bucket{
tools: map[string]struct{}{},
dataSources: map[string]struct{}{},
authSurfaces: map[string]struct{}{},
evidenceKeys: map[string]struct{}{},
}
}
for _, evidence := range finding.Evidence {
key := strings.ToLower(strings.TrimSpace(evidence.Key))
value := strings.TrimSpace(evidence.Value)
if key == "" || value == "" {
continue
}
switch key {
case "bound_tools", "tools", "tool", "mcp_server", "server":
for _, item := range splitCSV(value) {
entry.tools[item] = struct{}{}
entry.evidenceKeys["tool:"+item] = struct{}{}
}
case "data_sources", "data_source", "dataset", "table", "bucket":
for _, item := range splitCSV(value) {
entry.dataSources[item] = struct{}{}
entry.evidenceKeys["data:"+item] = struct{}{}
}
case "auth_surfaces", "auth", "auth_surface", "credentials", "credential":
for _, item := range splitCSV(value) {
entry.authSurfaces[item] = struct{}{}
entry.evidenceKeys["auth:"+item] = struct{}{}
}
}
}
resolved[instanceID] = entry
}

out := map[string]agginventory.AgentBindingContext{}
for instanceID, entry := range resolved {
bindings := agginventory.AgentBindingContext{
BoundTools: sortedKeys(entry.tools),
BoundDataSources: sortedKeys(entry.dataSources),
BoundAuthSurfaces: sortedKeys(entry.authSurfaces),
BindingEvidenceKeys: sortedKeys(entry.evidenceKeys),
}
if len(bindings.BoundTools) == 0 {
bindings.MissingBindings = append(bindings.MissingBindings, "tool_binding_unknown")
}
if len(bindings.BoundDataSources) == 0 {
bindings.MissingBindings = append(bindings.MissingBindings, "data_binding_unknown")
}
if len(bindings.BoundAuthSurfaces) == 0 {
bindings.MissingBindings = append(bindings.MissingBindings, "auth_binding_unknown")
}
if len(bindings.MissingBindings) > 0 {
sort.Strings(bindings.MissingBindings)
}
out[instanceID] = bindings
}
return out
}

func agentInstanceID(finding model.Finding) string {
symbol := ""
for _, evidence := range finding.Evidence {
key := strings.ToLower(strings.TrimSpace(evidence.Key))
if key == "symbol" || key == "name" || key == "agent_name" {
symbol = strings.TrimSpace(evidence.Value)
break
}
}
startLine := 0
endLine := 0
if finding.LocationRange != nil {
startLine = finding.LocationRange.StartLine
endLine = finding.LocationRange.EndLine
}
return identity.AgentInstanceID(finding.ToolType, finding.Location, symbol, startLine, endLine)
}

func splitCSV(value string) []string {
parts := strings.Split(value, ",")
out := make([]string, 0, len(parts))
for _, item := range parts {
trimmed := strings.TrimSpace(item)
if trimmed == "" {
continue
}
out = append(out, trimmed)
}
sort.Strings(out)
return out
}

func sortedKeys(in map[string]struct{}) []string {
if len(in) == 0 {
return nil
}
out := make([]string, 0, len(in))
for item := range in {
out = append(out, item)
}
sort.Strings(out)
return out
}
74 changes: 74 additions & 0 deletions core/aggregate/agentresolver/resolver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package agentresolver

import (
"reflect"
"testing"

"github.com/Clyra-AI/wrkr/core/identity"
"github.com/Clyra-AI/wrkr/core/model"
)

func TestAgentResolver_BindsToolsDataAuthDeterministically(t *testing.T) {
t.Parallel()

findings := []model.Finding{
{
FindingType: "agent_framework",
ToolType: "langchain",
Location: "agents/main.py",
Org: "acme",
Evidence: []model.Evidence{
{Key: "symbol", Value: "release_agent"},
{Key: "bound_tools", Value: "mcp.deploy,search.read"},
{Key: "data_sources", Value: "warehouse.events"},
{Key: "auth_surfaces", Value: "oauth2,token"},
},
},
}

first := Resolve(findings)
second := Resolve(findings)
if !reflect.DeepEqual(first, second) {
t.Fatalf("expected deterministic resolver output\nfirst=%+v\nsecond=%+v", first, second)
}

instanceID := identity.AgentInstanceID("langchain", "agents/main.py", "release_agent", 0, 0)
binding, ok := first[instanceID]
if !ok {
t.Fatalf("expected binding for %s", instanceID)
}
if !reflect.DeepEqual(binding.BoundTools, []string{"mcp.deploy", "search.read"}) {
t.Fatalf("unexpected bound tools: %+v", binding.BoundTools)
}
if !reflect.DeepEqual(binding.BoundDataSources, []string{"warehouse.events"}) {
t.Fatalf("unexpected data sources: %+v", binding.BoundDataSources)
}
if !reflect.DeepEqual(binding.BoundAuthSurfaces, []string{"oauth2", "token"}) {
t.Fatalf("unexpected auth surfaces: %+v", binding.BoundAuthSurfaces)
}
if len(binding.BindingEvidenceKeys) == 0 {
t.Fatalf("expected stable evidence keys, got %+v", binding.BindingEvidenceKeys)
}
}

func TestAgentResolver_PartialExtractionMarksMissingLinks(t *testing.T) {
t.Parallel()

findings := []model.Finding{
{
FindingType: "agent_framework",
ToolType: "crewai",
Location: "crews/ops.py",
Evidence: []model.Evidence{
{Key: "symbol", Value: "ops_agent"},
{Key: "bound_tools", Value: "deploy.write"},
},
},
}

instanceID := identity.AgentInstanceID("crewai", "crews/ops.py", "ops_agent", 0, 0)
binding := Resolve(findings)[instanceID]
if !reflect.DeepEqual(binding.MissingBindings, []string{"auth_binding_unknown", "data_binding_unknown"}) {
t.Fatalf("expected deterministic missing links, got %+v", binding.MissingBindings)
}
}
Loading