diff --git a/internal/aiagentconfig/aiagentconfig.go b/internal/aiagentconfig/aiagentconfig.go index 7ed2da8b5..a25f31cc2 100644 --- a/internal/aiagentconfig/aiagentconfig.go +++ b/internal/aiagentconfig/aiagentconfig.go @@ -15,6 +15,22 @@ package aiagentconfig +// ConfigFileKind classifies the purpose of a discovered configuration file. +type ConfigFileKind string + +const ( + // ConfigFileKindConfiguration is for settings and JSON config files. + ConfigFileKindConfiguration ConfigFileKind = "configuration" + // ConfigFileKindInstruction is for markdown instruction/rules files. + ConfigFileKindInstruction ConfigFileKind = "instruction" +) + +// DiscoveredFile represents a file found during discovery, before reading its content. +type DiscoveredFile struct { + Path string + Kind ConfigFileKind +} + // Agent identifies the AI agent provider type Agent struct { Name string `json:"name"` @@ -30,10 +46,11 @@ type GitContext struct { // ConfigFile represents a single discovered configuration file type ConfigFile struct { - Path string `json:"path"` - SHA256 string `json:"sha256"` - Size int64 `json:"size"` - Content string `json:"content"` + Path string `json:"path"` + Kind ConfigFileKind `json:"kind"` + SHA256 string `json:"sha256"` + Size int64 `json:"size"` + Content string `json:"content"` } // Evidence is the AI agent configuration payload diff --git a/internal/aiagentconfig/builder.go b/internal/aiagentconfig/builder.go index 97c550d21..197df65cc 100644 --- a/internal/aiagentconfig/builder.go +++ b/internal/aiagentconfig/builder.go @@ -30,20 +30,21 @@ import ( ) // Build reads discovered files and constructs the AI agent config payload. -// basePath is the base directory, filePaths are relative to basePath. +// basePath is the base directory, discovered contains files relative to basePath with their kinds. // agentName identifies the AI agent (e.g. "claude", "cursor"). // gitCtx may be nil if not in a git repository. -func Build(basePath string, filePaths []string, agentName string, gitCtx *GitContext) (*Evidence, error) { +func Build(basePath string, discovered []DiscoveredFile, agentName string, gitCtx *GitContext) (*Evidence, error) { // Resolve basePath to its real path so symlink comparisons are reliable realRoot, err := filepath.EvalSymlinks(basePath) if err != nil { return nil, fmt.Errorf("resolving root dir: %w", err) } - configFiles := make([]ConfigFile, 0, len(filePaths)) - hashes := make([]string, 0, len(filePaths)) + configFiles := make([]ConfigFile, 0, len(discovered)) + hashes := make([]string, 0, len(discovered)) - for _, relPath := range filePaths { + for _, df := range discovered { + relPath := df.Path absPath := filepath.Join(basePath, relPath) // Resolve the full path through any symlinks (covers both symlinked @@ -73,6 +74,7 @@ func Build(basePath string, filePaths []string, agentName string, gitCtx *GitCon configFiles = append(configFiles, ConfigFile{ Path: relPath, + Kind: df.Kind, SHA256: hexHash, Size: info.Size(), Content: base64.StdEncoding.EncodeToString(content), diff --git a/internal/aiagentconfig/builder_test.go b/internal/aiagentconfig/builder_test.go index ca6bda7cf..02607dcae 100644 --- a/internal/aiagentconfig/builder_test.go +++ b/internal/aiagentconfig/builder_test.go @@ -47,7 +47,10 @@ func TestBuild(t *testing.T) { CommitSHA: "abc123", } - data, err := Build(rootDir, []string{"CLAUDE.md", ".claude/settings.json"}, "claude", gitCtx) + data, err := Build(rootDir, []DiscoveredFile{ + {Path: "CLAUDE.md", Kind: ConfigFileKindInstruction}, + {Path: ".claude/settings.json", Kind: ConfigFileKindConfiguration}, + }, "claude", gitCtx) require.NoError(t, err) assert.Equal(t, "0.1", data.SchemaVersion) @@ -65,6 +68,7 @@ func TestBuild(t *testing.T) { cf1 := data.ConfigFiles[0] assert.Equal(t, "CLAUDE.md", cf1.Path) + assert.Equal(t, ConfigFileKindInstruction, cf1.Kind) assert.Equal(t, int64(len(file1Content)), cf1.Size) hash1 := sha256.Sum256(file1Content) assert.Equal(t, hex.EncodeToString(hash1[:]), cf1.SHA256) @@ -72,6 +76,7 @@ func TestBuild(t *testing.T) { cf2 := data.ConfigFiles[1] assert.Equal(t, ".claude/settings.json", cf2.Path) + assert.Equal(t, ConfigFileKindConfiguration, cf2.Kind) hash2 := sha256.Sum256(file2Content) assert.Equal(t, hex.EncodeToString(hash2[:]), cf2.SHA256) @@ -91,19 +96,24 @@ func TestBuildWithCursorAgent(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(rootDir, ".cursor", "rules"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(rootDir, ".cursor", "rules", "coding.md"), []byte("rules"), 0o600)) - data, err := Build(rootDir, []string{".cursor/rules/coding.md"}, "cursor", nil) + data, err := Build(rootDir, []DiscoveredFile{ + {Path: ".cursor/rules/coding.md", Kind: ConfigFileKindInstruction}, + }, "cursor", nil) require.NoError(t, err) assert.Equal(t, "cursor", data.Agent.Name) require.Len(t, data.ConfigFiles, 1) assert.Equal(t, ".cursor/rules/coding.md", data.ConfigFiles[0].Path) + assert.Equal(t, ConfigFileKindInstruction, data.ConfigFiles[0].Kind) } func TestBuildWithoutGitContext(t *testing.T) { rootDir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(rootDir, "CLAUDE.md"), []byte("content"), 0o600)) - data, err := Build(rootDir, []string{"CLAUDE.md"}, "claude", nil) + data, err := Build(rootDir, []DiscoveredFile{ + {Path: "CLAUDE.md", Kind: ConfigFileKindInstruction}, + }, "claude", nil) require.NoError(t, err) assert.Nil(t, data.GitContext) @@ -114,7 +124,9 @@ func TestBuildJSONFormat(t *testing.T) { rootDir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(rootDir, "CLAUDE.md"), []byte("content"), 0o600)) - data, err := Build(rootDir, []string{"CLAUDE.md"}, "claude", nil) + data, err := Build(rootDir, []DiscoveredFile{ + {Path: "CLAUDE.md", Kind: ConfigFileKindInstruction}, + }, "claude", nil) require.NoError(t, err) // Verify it marshals to valid JSON with top-level fields (no envelope) @@ -132,6 +144,12 @@ func TestBuildJSONFormat(t *testing.T) { // Ensure no envelope fields assert.Nil(t, raw["chainloop.material.evidence.id"]) assert.Nil(t, raw["data"]) + + // Verify kind is present in config_files + files := raw["config_files"].([]any) + require.Len(t, files, 1) + file := files[0].(map[string]any) + assert.Equal(t, "instruction", file["kind"]) } func TestBuildRejectsSymlinksEscapingRoot(t *testing.T) { @@ -142,7 +160,9 @@ func TestBuildRejectsSymlinksEscapingRoot(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(outsideDir, "secret.txt"), []byte("secret"), 0o600)) require.NoError(t, os.Symlink(filepath.Join(outsideDir, "secret.txt"), filepath.Join(rootDir, "CLAUDE.md"))) - _, err := Build(rootDir, []string{"CLAUDE.md"}, "claude", nil) + _, err := Build(rootDir, []DiscoveredFile{ + {Path: "CLAUDE.md", Kind: ConfigFileKindInstruction}, + }, "claude", nil) require.Error(t, err) assert.Contains(t, err.Error(), "path escapes root directory via symlink") } @@ -159,7 +179,9 @@ func TestBuildRejectsSymlinkedParentDir(t *testing.T) { // Symlink .claude -> outside directory require.NoError(t, os.Symlink(outsideClaude, filepath.Join(rootDir, ".claude"))) - _, err := Build(rootDir, []string{".claude/settings.json"}, "claude", nil) + _, err := Build(rootDir, []DiscoveredFile{ + {Path: ".claude/settings.json", Kind: ConfigFileKindConfiguration}, + }, "claude", nil) require.Error(t, err) assert.Contains(t, err.Error(), "path escapes root directory via symlink") } @@ -167,7 +189,7 @@ func TestBuildRejectsSymlinkedParentDir(t *testing.T) { func TestBuildEmptyFileList(t *testing.T) { rootDir := t.TempDir() - data, err := Build(rootDir, []string{}, "claude", nil) + data, err := Build(rootDir, []DiscoveredFile{}, "claude", nil) require.NoError(t, err) assert.Empty(t, data.ConfigFiles) assert.NotEmpty(t, data.ConfigHash) @@ -186,7 +208,9 @@ func TestBuildAllowsRegularFilesInRoot(t *testing.T) { rootDir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(rootDir, "CLAUDE.md"), []byte("content"), 0o600)) - data, err := Build(rootDir, []string{"CLAUDE.md"}, "claude", nil) + data, err := Build(rootDir, []DiscoveredFile{ + {Path: "CLAUDE.md", Kind: ConfigFileKindInstruction}, + }, "claude", nil) require.NoError(t, err) assert.Len(t, data.ConfigFiles, 1) } diff --git a/internal/aiagentconfig/discover.go b/internal/aiagentconfig/discover.go index ceaf41ec8..c9b878be3 100644 --- a/internal/aiagentconfig/discover.go +++ b/internal/aiagentconfig/discover.go @@ -20,51 +20,57 @@ import ( "sort" ) +// patternDef pairs a glob pattern with the kind of file it matches. +type patternDef struct { + pattern string + kind ConfigFileKind +} + // agentDef defines an AI agent and its exclusive file patterns. type agentDef struct { name string - patterns []string + patterns []patternDef } // agents is the registry of supported AI agents and their exclusive file patterns. var agents = []agentDef{ - {name: "claude", patterns: []string{ - "CLAUDE.md", - ".claude/CLAUDE.md", - ".claude/settings.json", - ".claude/rules/*.md", - ".claude/agents/*.md", - ".claude/commands/*.md", - ".claude/skills/*/SKILL.md", + {name: "claude", patterns: []patternDef{ + {"CLAUDE.md", ConfigFileKindInstruction}, + {".claude/CLAUDE.md", ConfigFileKindInstruction}, + {".claude/settings.json", ConfigFileKindConfiguration}, + {".claude/rules/*.md", ConfigFileKindInstruction}, + {".claude/agents/*.md", ConfigFileKindInstruction}, + {".claude/commands/*.md", ConfigFileKindInstruction}, + {".claude/skills/*/SKILL.md", ConfigFileKindInstruction}, }}, - {name: "cursor", patterns: []string{ - ".cursor/rules/*.md", - ".cursor/rules/*.mdc", - ".cursor/rules/*/*.md", - ".cursor/rules/*/*.mdc", - ".cursor/skills/*/SKILL.md", - ".cursor/agents/*.md", + {name: "cursor", patterns: []patternDef{ + {".cursor/rules/*.md", ConfigFileKindInstruction}, + {".cursor/rules/*.mdc", ConfigFileKindInstruction}, + {".cursor/rules/*/*.md", ConfigFileKindInstruction}, + {".cursor/rules/*/*.mdc", ConfigFileKindInstruction}, + {".cursor/skills/*/SKILL.md", ConfigFileKindInstruction}, + {".cursor/agents/*.md", ConfigFileKindInstruction}, }}, } // sharedPatterns are file patterns not exclusive to any agent. // They are included in every agent's evidence when that agent has exclusive files. -var sharedPatterns = []string{ - ".mcp.json", - "AGENTS.md", +var sharedPatterns = []patternDef{ + {".mcp.json", ConfigFileKindConfiguration}, + {"AGENTS.md", ConfigFileKindInstruction}, } // DiscoverAll searches basePath for AI agent configuration files and groups them by agent. // Only agents with at least one exclusive file match are included. // Shared files are appended to each detected agent's file list. -// Returns a map of agent name → sorted, deduplicated relative paths. -func DiscoverAll(basePath string) (map[string][]string, error) { +// Returns a map of agent name → sorted, deduplicated discovered files. +func DiscoverAll(basePath string) (map[string][]DiscoveredFile, error) { sharedFiles, err := matchPatterns(basePath, sharedPatterns) if err != nil { return nil, err } - result := make(map[string][]string) + result := make(map[string][]DiscoveredFile) for _, agent := range agents { files, err := matchPatterns(basePath, agent.patterns) @@ -78,18 +84,18 @@ func DiscoverAll(basePath string) (map[string][]string, error) { // Merge shared files into this agent's list, deduplicating seen := make(map[string]struct{}, len(files)+len(sharedFiles)) - merged := make([]string, 0, len(files)+len(sharedFiles)) + merged := make([]DiscoveredFile, 0, len(files)+len(sharedFiles)) for _, f := range files { - seen[f] = struct{}{} + seen[f.Path] = struct{}{} merged = append(merged, f) } for _, f := range sharedFiles { - if _, ok := seen[f]; !ok { + if _, ok := seen[f.Path]; !ok { merged = append(merged, f) } } - sort.Strings(merged) + sort.Slice(merged, func(i, j int) bool { return merged[i].Path < merged[j].Path }) result[agent.name] = merged } @@ -97,13 +103,13 @@ func DiscoverAll(basePath string) (map[string][]string, error) { } // matchPatterns expands glob patterns relative to basePath and returns -// deduplicated, sorted relative paths. -func matchPatterns(basePath string, patterns []string) ([]string, error) { +// deduplicated, sorted discovered files. +func matchPatterns(basePath string, patterns []patternDef) ([]DiscoveredFile, error) { seen := make(map[string]struct{}) - var results []string + var results []DiscoveredFile - for _, pattern := range patterns { - absPattern := filepath.Join(basePath, pattern) + for _, pd := range patterns { + absPattern := filepath.Join(basePath, pd.pattern) matches, err := filepath.Glob(absPattern) if err != nil { return nil, err @@ -117,12 +123,12 @@ func matchPatterns(basePath string, patterns []string) ([]string, error) { if _, ok := seen[rel]; !ok { seen[rel] = struct{}{} - results = append(results, rel) + results = append(results, DiscoveredFile{Path: rel, Kind: pd.kind}) } } } - sort.Strings(results) + sort.Slice(results, func(i, j int) bool { return results[i].Path < results[j].Path }) return results, nil } diff --git a/internal/aiagentconfig/discover_test.go b/internal/aiagentconfig/discover_test.go index 588c12433..f33d1fe15 100644 --- a/internal/aiagentconfig/discover_test.go +++ b/internal/aiagentconfig/discover_test.go @@ -38,25 +38,31 @@ func TestDiscoverAll(t *testing.T) { tests := []struct { name string files []string - expected map[string][]string + expected map[string][]DiscoveredFile }{ { name: "no config files", files: []string{"main.go", "README.md"}, - expected: map[string][]string{}, + expected: map[string][]DiscoveredFile{}, }, { name: "claude only", files: []string{"CLAUDE.md", ".claude/settings.json"}, - expected: map[string][]string{ - "claude": {".claude/settings.json", "CLAUDE.md"}, + expected: map[string][]DiscoveredFile{ + "claude": { + {Path: ".claude/settings.json", Kind: ConfigFileKindConfiguration}, + {Path: "CLAUDE.md", Kind: ConfigFileKindInstruction}, + }, }, }, { name: "cursor only", files: []string{".cursor/rules/coding.md", ".cursor/agents/test.md"}, - expected: map[string][]string{ - "cursor": {".cursor/agents/test.md", ".cursor/rules/coding.md"}, + expected: map[string][]DiscoveredFile{ + "cursor": { + {Path: ".cursor/agents/test.md", Kind: ConfigFileKindInstruction}, + {Path: ".cursor/rules/coding.md", Kind: ConfigFileKindInstruction}, + }, }, }, { @@ -65,8 +71,11 @@ func TestDiscoverAll(t *testing.T) { ".cursor/rules/react.mdc", ".cursor/rules/api.md", }, - expected: map[string][]string{ - "cursor": {".cursor/rules/api.md", ".cursor/rules/react.mdc"}, + expected: map[string][]DiscoveredFile{ + "cursor": { + {Path: ".cursor/rules/api.md", Kind: ConfigFileKindInstruction}, + {Path: ".cursor/rules/react.mdc", Kind: ConfigFileKindInstruction}, + }, }, }, { @@ -75,15 +84,20 @@ func TestDiscoverAll(t *testing.T) { ".cursor/rules/frontend/components.md", ".cursor/rules/backend/api.mdc", }, - expected: map[string][]string{ - "cursor": {".cursor/rules/backend/api.mdc", ".cursor/rules/frontend/components.md"}, + expected: map[string][]DiscoveredFile{ + "cursor": { + {Path: ".cursor/rules/backend/api.mdc", Kind: ConfigFileKindInstruction}, + {Path: ".cursor/rules/frontend/components.md", Kind: ConfigFileKindInstruction}, + }, }, }, { name: "cursor skills", files: []string{".cursor/skills/search/SKILL.md"}, - expected: map[string][]string{ - "cursor": {".cursor/skills/search/SKILL.md"}, + expected: map[string][]DiscoveredFile{ + "cursor": { + {Path: ".cursor/skills/search/SKILL.md", Kind: ConfigFileKindInstruction}, + }, }, }, { @@ -94,9 +108,15 @@ func TestDiscoverAll(t *testing.T) { ".cursor/rules/coding.md", ".cursor/agents/reviewer.md", }, - expected: map[string][]string{ - "claude": {".claude/settings.json", "CLAUDE.md"}, - "cursor": {".cursor/agents/reviewer.md", ".cursor/rules/coding.md"}, + expected: map[string][]DiscoveredFile{ + "claude": { + {Path: ".claude/settings.json", Kind: ConfigFileKindConfiguration}, + {Path: "CLAUDE.md", Kind: ConfigFileKindInstruction}, + }, + "cursor": { + {Path: ".cursor/agents/reviewer.md", Kind: ConfigFileKindInstruction}, + {Path: ".cursor/rules/coding.md", Kind: ConfigFileKindInstruction}, + }, }, }, { @@ -107,15 +127,23 @@ func TestDiscoverAll(t *testing.T) { ".mcp.json", "AGENTS.md", }, - expected: map[string][]string{ - "claude": {".mcp.json", "AGENTS.md", "CLAUDE.md"}, - "cursor": {".cursor/rules/coding.md", ".mcp.json", "AGENTS.md"}, + expected: map[string][]DiscoveredFile{ + "claude": { + {Path: ".mcp.json", Kind: ConfigFileKindConfiguration}, + {Path: "AGENTS.md", Kind: ConfigFileKindInstruction}, + {Path: "CLAUDE.md", Kind: ConfigFileKindInstruction}, + }, + "cursor": { + {Path: ".cursor/rules/coding.md", Kind: ConfigFileKindInstruction}, + {Path: ".mcp.json", Kind: ConfigFileKindConfiguration}, + {Path: "AGENTS.md", Kind: ConfigFileKindInstruction}, + }, }, }, { name: "only shared files - no agents returned", files: []string{".mcp.json", "AGENTS.md"}, - expected: map[string][]string{}, + expected: map[string][]DiscoveredFile{}, }, { name: "all claude patterns with shared", @@ -131,18 +159,18 @@ func TestDiscoverAll(t *testing.T) { ".claude/commands/deploy.md", ".claude/skills/search/SKILL.md", }, - expected: map[string][]string{ + expected: map[string][]DiscoveredFile{ "claude": { - ".claude/CLAUDE.md", - ".claude/agents/reviewer.md", - ".claude/commands/deploy.md", - ".claude/rules/coding.md", - ".claude/rules/testing.md", - ".claude/settings.json", - ".claude/skills/search/SKILL.md", - ".mcp.json", - "AGENTS.md", - "CLAUDE.md", + {Path: ".claude/CLAUDE.md", Kind: ConfigFileKindInstruction}, + {Path: ".claude/agents/reviewer.md", Kind: ConfigFileKindInstruction}, + {Path: ".claude/commands/deploy.md", Kind: ConfigFileKindInstruction}, + {Path: ".claude/rules/coding.md", Kind: ConfigFileKindInstruction}, + {Path: ".claude/rules/testing.md", Kind: ConfigFileKindInstruction}, + {Path: ".claude/settings.json", Kind: ConfigFileKindConfiguration}, + {Path: ".claude/skills/search/SKILL.md", Kind: ConfigFileKindInstruction}, + {Path: ".mcp.json", Kind: ConfigFileKindConfiguration}, + {Path: "AGENTS.md", Kind: ConfigFileKindInstruction}, + {Path: "CLAUDE.md", Kind: ConfigFileKindInstruction}, }, }, }, @@ -155,8 +183,10 @@ func TestDiscoverAll(t *testing.T) { "some/nested/CLAUDE.md", // nested too deep ".cursor/other/random.md", // not a known pattern }, - expected: map[string][]string{ - "claude": {"CLAUDE.md"}, + expected: map[string][]DiscoveredFile{ + "claude": { + {Path: "CLAUDE.md", Kind: ConfigFileKindInstruction}, + }, }, }, } diff --git a/internal/schemavalidators/internal_schemas/aiagentconfig/ai-agent-config-0.1.schema.json b/internal/schemavalidators/internal_schemas/aiagentconfig/ai-agent-config-0.1.schema.json index 8e0c30558..c56211f2b 100644 --- a/internal/schemavalidators/internal_schemas/aiagentconfig/ai-agent-config-0.1.schema.json +++ b/internal/schemavalidators/internal_schemas/aiagentconfig/ai-agent-config-0.1.schema.json @@ -65,12 +65,17 @@ "description": "Array of discovered configuration files", "items": { "type": "object", - "required": ["path", "sha256", "size", "content"], + "required": ["path", "kind", "sha256", "size", "content"], "properties": { "path": { "type": "string", "description": "Relative file path" }, + "kind": { + "type": "string", + "enum": ["configuration", "instruction"], + "description": "Classification of the file's purpose" + }, "sha256": { "type": "string", "description": "SHA-256 digest of the file" diff --git a/internal/schemavalidators/testdata/ai_agent_config_extra_fields.json b/internal/schemavalidators/testdata/ai_agent_config_extra_fields.json index 55426735f..567ccf50b 100644 --- a/internal/schemavalidators/testdata/ai_agent_config_extra_fields.json +++ b/internal/schemavalidators/testdata/ai_agent_config_extra_fields.json @@ -8,6 +8,7 @@ "config_files": [ { "path": "CLAUDE.md", + "kind": "instruction", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 10, "content": "Y29udGVudA==" diff --git a/internal/schemavalidators/testdata/ai_agent_config_minimal.json b/internal/schemavalidators/testdata/ai_agent_config_minimal.json index 12fdda8b3..2f1916261 100644 --- a/internal/schemavalidators/testdata/ai_agent_config_minimal.json +++ b/internal/schemavalidators/testdata/ai_agent_config_minimal.json @@ -8,6 +8,7 @@ "config_files": [ { "path": "CLAUDE.md", + "kind": "instruction", "sha256": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73", "size": 7, "content": "Y29udGVudA==" diff --git a/internal/schemavalidators/testdata/ai_agent_config_valid.json b/internal/schemavalidators/testdata/ai_agent_config_valid.json index b1f35e060..75ab387a2 100644 --- a/internal/schemavalidators/testdata/ai_agent_config_valid.json +++ b/internal/schemavalidators/testdata/ai_agent_config_valid.json @@ -14,6 +14,7 @@ "config_files": [ { "path": "CLAUDE.md", + "kind": "instruction", "sha256": "4c3b798d41eb62a633e0954b2cbdd4b388ca0b83f84a89cc7936cf8575d7349e", "size": 15, "content": "IyBQcm9qZWN0IFJ1bGVz" diff --git a/pkg/attestation/crafter/collector_aiagentconfig.go b/pkg/attestation/crafter/collector_aiagentconfig.go index b5bf151a8..2b88734d3 100644 --- a/pkg/attestation/crafter/collector_aiagentconfig.go +++ b/pkg/attestation/crafter/collector_aiagentconfig.go @@ -68,8 +68,13 @@ func (c *AIAgentConfigCollector) Collect(ctx context.Context, cr *Crafter, attes for _, agentName := range agentNames { files := agentFiles[agentName] + paths := make([]string, len(files)) + for i, f := range files { + paths[i] = f.Path + } + cr.Logger.Info().Str("agent", agentName).Int("files", len(files)).Msg("discovered AI agent config files") - cr.Logger.Debug().Str("agent", agentName).Strs("paths", files).Msg("AI agent config file paths") + cr.Logger.Debug().Str("agent", agentName).Strs("paths", paths).Msg("AI agent config file paths") if err := c.uploadAgentConfig(ctx, cr, attestationID, casBackend, agentName, files, gitCtx); err != nil { return err @@ -81,7 +86,7 @@ func (c *AIAgentConfigCollector) Collect(ctx context.Context, cr *Crafter, attes func (c *AIAgentConfigCollector) uploadAgentConfig( ctx context.Context, cr *Crafter, attestationID string, - casBackend *casclient.CASBackend, agentName string, files []string, gitCtx *aiagentconfig.GitContext, + casBackend *casclient.CASBackend, agentName string, files []aiagentconfig.DiscoveredFile, gitCtx *aiagentconfig.GitContext, ) error { evidence, err := aiagentconfig.Build(cr.WorkingDir(), files, agentName, gitCtx) if err != nil { diff --git a/pkg/attestation/crafter/materials/chainloop_ai_agent_config_test.go b/pkg/attestation/crafter/materials/chainloop_ai_agent_config_test.go index 4c33ae4d3..ecd2d6499 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_agent_config_test.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_agent_config_test.go @@ -77,6 +77,7 @@ func TestChainloopAIAgentConfigCrafter_Validation(t *testing.T) { ConfigFiles: []aiagentconfig.ConfigFile{ { Path: "CLAUDE.md", + Kind: aiagentconfig.ConfigFileKindInstruction, SHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 42, Content: "IyBQcm9qZWN0IFJ1bGVz", @@ -95,6 +96,7 @@ func TestChainloopAIAgentConfigCrafter_Validation(t *testing.T) { ConfigFiles: []aiagentconfig.ConfigFile{ { Path: "CLAUDE.md", + Kind: aiagentconfig.ConfigFileKindInstruction, SHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 10, Content: "Y29udGVudA==", @@ -113,6 +115,7 @@ func TestChainloopAIAgentConfigCrafter_Validation(t *testing.T) { ConfigFiles: []aiagentconfig.ConfigFile{ { Path: ".cursor/rules/coding.md", + Kind: aiagentconfig.ConfigFileKindInstruction, SHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 20, Content: "IyBDb2RpbmcgUnVsZXM=", @@ -131,18 +134,21 @@ func TestChainloopAIAgentConfigCrafter_Validation(t *testing.T) { ConfigFiles: []aiagentconfig.ConfigFile{ { Path: ".cursor/rules/react.mdc", + Kind: aiagentconfig.ConfigFileKindInstruction, SHA256: "abc123", Size: 15, Content: "cnVsZXM=", }, { Path: ".cursor/agents/reviewer.md", + Kind: aiagentconfig.ConfigFileKindInstruction, SHA256: "def456", Size: 10, Content: "YWdlbnQ=", }, { Path: "AGENTS.md", + Kind: aiagentconfig.ConfigFileKindInstruction, SHA256: "789abc", Size: 8, Content: "YWdlbnRz", @@ -251,7 +257,7 @@ func TestChainloopAIAgentConfigCrafter_RejectsExtraFields(t *testing.T) { "agent": {"name": "claude"}, "config_hash": "abc", "captured_at": "2026-03-13T10:00:00Z", - "config_files": [{"path": "CLAUDE.md", "sha256": "abc", "size": 1, "content": "Yg=="}], + "config_files": [{"path": "CLAUDE.md", "kind": "instruction", "sha256": "abc", "size": 1, "content": "Yg=="}], "unknown_field": "should fail" }` diff --git a/pkg/attestation/crafter/materials/testdata/ai-agent-config-claude.json b/pkg/attestation/crafter/materials/testdata/ai-agent-config-claude.json index 3e4baefdf..1e00d3dbf 100644 --- a/pkg/attestation/crafter/materials/testdata/ai-agent-config-claude.json +++ b/pkg/attestation/crafter/materials/testdata/ai-agent-config-claude.json @@ -6,6 +6,7 @@ "config_files": [ { "path": "CLAUDE.md", + "kind": "instruction", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 42, "content": "IyBQcm9qZWN0IFJ1bGVz" diff --git a/pkg/attestation/crafter/materials/testdata/ai-agent-config-cursor.json b/pkg/attestation/crafter/materials/testdata/ai-agent-config-cursor.json index af09b9b8e..96159ce36 100644 --- a/pkg/attestation/crafter/materials/testdata/ai-agent-config-cursor.json +++ b/pkg/attestation/crafter/materials/testdata/ai-agent-config-cursor.json @@ -6,6 +6,7 @@ "config_files": [ { "path": ".cursor/rules/coding.md", + "kind": "instruction", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 20, "content": "IyBDb2RpbmcgUnVsZXM="