Skip to content

Commit 55d900f

Browse files
authored
feat: wrap AI agent config in evidence envelope (#2870)
Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
1 parent 7b0148e commit 55d900f

13 files changed

Lines changed: 125 additions & 102 deletions

internal/aiagentconfig/aiagentconfig.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ const (
2323
ConfigFileKindConfiguration ConfigFileKind = "configuration"
2424
// ConfigFileKindInstruction is for markdown instruction/rules files.
2525
ConfigFileKindInstruction ConfigFileKind = "instruction"
26+
27+
// EvidenceID is the identifier for the AI agent config material type
28+
EvidenceID = "CHAINLOOP_AI_AGENT_CONFIG"
29+
// EvidenceSchemaURL is the URL to the JSON schema for AI agent config
30+
EvidenceSchemaURL = "https://schemas.chainloop.dev/aiagentconfig/0.1/ai-agent-config.schema.json"
2631
)
2732

2833
// DiscoveredFile represents a file found during discovery, before reading its content.
@@ -53,16 +58,31 @@ type ConfigFile struct {
5358
Content string `json:"content"`
5459
}
5560

56-
// Evidence is the AI agent configuration payload
57-
type Evidence struct {
58-
SchemaVersion string `json:"schema_version"`
59-
Agent Agent `json:"agent"`
60-
ConfigHash string `json:"config_hash"`
61-
CapturedAt string `json:"captured_at"`
62-
GitContext *GitContext `json:"git_context,omitempty"`
63-
ConfigFiles []ConfigFile `json:"config_files"`
61+
// Data is the AI agent configuration payload
62+
type Data struct {
63+
Agent Agent `json:"agent"`
64+
ConfigHash string `json:"config_hash"`
65+
CapturedAt string `json:"captured_at"`
66+
GitContext *GitContext `json:"git_context,omitempty"`
67+
ConfigFiles []ConfigFile `json:"config_files"`
6468
// Future fields for richer analysis
6569
Permissions any `json:"permissions,omitempty"`
6670
MCPServers any `json:"mcp_servers,omitempty"`
6771
Subagents any `json:"subagents,omitempty"`
6872
}
73+
74+
// Evidence represents the complete evidence structure for AI agent config
75+
type Evidence struct {
76+
ID string `json:"chainloop.material.evidence.id"`
77+
Schema string `json:"schema"`
78+
Data Data `json:"data"`
79+
}
80+
81+
// NewEvidence creates a new Evidence instance
82+
func NewEvidence(data Data) *Evidence {
83+
return &Evidence{
84+
ID: EvidenceID,
85+
Schema: EvidenceSchemaURL,
86+
Data: data,
87+
}
88+
}

internal/aiagentconfig/builder.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,13 @@ import (
2525
"sort"
2626
"strings"
2727
"time"
28-
29-
"github.com/chainloop-dev/chainloop/internal/schemavalidators"
3028
)
3129

3230
// Build reads discovered files and constructs the AI agent config payload.
3331
// basePath is the base directory, discovered contains files relative to basePath with their kinds.
3432
// agentName identifies the AI agent (e.g. "claude", "cursor").
3533
// gitCtx may be nil if not in a git repository.
36-
func Build(basePath string, discovered []DiscoveredFile, agentName string, gitCtx *GitContext) (*Evidence, error) {
34+
func Build(basePath string, discovered []DiscoveredFile, agentName string, gitCtx *GitContext) (*Data, error) {
3735
// Resolve basePath to its real path so symlink comparisons are reliable
3836
realRoot, err := filepath.EvalSymlinks(basePath)
3937
if err != nil {
@@ -81,13 +79,12 @@ func Build(basePath string, discovered []DiscoveredFile, agentName string, gitCt
8179
})
8280
}
8381

84-
data := Evidence{
85-
SchemaVersion: string(schemavalidators.AIAgentConfigVersion0_1),
86-
Agent: Agent{Name: agentName},
87-
ConfigHash: computeCombinedHash(hashes),
88-
CapturedAt: time.Now().UTC().Format(time.RFC3339),
89-
GitContext: gitCtx,
90-
ConfigFiles: configFiles,
82+
data := Data{
83+
Agent: Agent{Name: agentName},
84+
ConfigHash: computeCombinedHash(hashes),
85+
CapturedAt: time.Now().UTC().Format(time.RFC3339),
86+
GitContext: gitCtx,
87+
ConfigFiles: configFiles,
9188
}
9289

9390
return &data, nil

internal/aiagentconfig/builder_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ func TestBuild(t *testing.T) {
5353
}, "claude", gitCtx)
5454
require.NoError(t, err)
5555

56-
assert.Equal(t, "0.1", data.SchemaVersion)
5756
assert.Equal(t, "claude", data.Agent.Name)
5857
assert.NotEmpty(t, data.CapturedAt)
5958
assert.NotEmpty(t, data.ConfigHash)
@@ -136,7 +135,6 @@ func TestBuildJSONFormat(t *testing.T) {
136135
var raw map[string]any
137136
require.NoError(t, json.Unmarshal(jsonData, &raw))
138137

139-
assert.NotNil(t, raw["schema_version"])
140138
assert.NotNil(t, raw["agent"])
141139
assert.NotNil(t, raw["config_hash"])
142140
assert.NotNil(t, raw["captured_at"])

internal/schemavalidators/internal_schemas/aiagentconfig/ai-agent-config-0.1.schema.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,12 @@
55
"title": "AI Agent Configuration",
66
"description": "Schema for AI agent configuration data collected during attestation",
77
"required": [
8-
"schema_version",
98
"agent",
109
"config_hash",
1110
"captured_at",
1211
"config_files"
1312
],
1413
"properties": {
15-
"schema_version": {
16-
"type": "string",
17-
"description": "Schema version identifier"
18-
},
1914
"agent": {
2015
"type": "object",
2116
"description": "AI agent provider information",

internal/schemavalidators/testdata/ai_agent_config_empty_config_files.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"schema_version": "0.1",
32
"agent": {
43
"name": "claude"
54
},

internal/schemavalidators/testdata/ai_agent_config_extra_fields.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"schema_version": "0.1",
32
"agent": {
43
"name": "claude"
54
},

internal/schemavalidators/testdata/ai_agent_config_minimal.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"schema_version": "0.1",
32
"agent": {
43
"name": "claude"
54
},

internal/schemavalidators/testdata/ai_agent_config_valid.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"schema_version": "0.1",
32
"agent": {
43
"name": "claude",
54
"version": "4.0"

pkg/attestation/crafter/collector_aiagentconfig.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,12 @@ func (c *AIAgentConfigCollector) uploadAgentConfig(
8888
ctx context.Context, cr *Crafter, attestationID string,
8989
casBackend *casclient.CASBackend, agentName string, files []aiagentconfig.DiscoveredFile, gitCtx *aiagentconfig.GitContext,
9090
) error {
91-
evidence, err := aiagentconfig.Build(cr.WorkingDir(), files, agentName, gitCtx)
91+
data, err := aiagentconfig.Build(cr.WorkingDir(), files, agentName, gitCtx)
9292
if err != nil {
9393
return fmt.Errorf("building AI agent config for %s: %w", agentName, err)
9494
}
9595

96+
evidence := aiagentconfig.NewEvidence(*data)
9697
jsonData, err := json.Marshal(evidence)
9798
if err != nil {
9899
return fmt.Errorf("marshaling AI agent config for %s: %w", agentName, err)

pkg/attestation/crafter/materials/chainloop_ai_agent_config.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os"
2323

2424
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
25+
"github.com/chainloop-dev/chainloop/internal/aiagentconfig"
2526
"github.com/chainloop-dev/chainloop/internal/schemavalidators"
2627
api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
2728
"github.com/chainloop-dev/chainloop/pkg/casclient"
@@ -55,12 +56,28 @@ func (c *ChainloopAIAgentConfigCrafter) Craft(ctx context.Context, artifactPath
5556
return nil, fmt.Errorf("can't open the file: %w", err)
5657
}
5758

58-
var rawData any
59-
if err := json.Unmarshal(f, &rawData); err != nil {
59+
// Unmarshal envelope, keeping data as raw JSON for schema validation
60+
var envelope struct {
61+
Data json.RawMessage `json:"data"`
62+
}
63+
if err := json.Unmarshal(f, &envelope); err != nil {
6064
c.logger.Debug().Err(err).Msg("error decoding file")
6165
return nil, fmt.Errorf("invalid JSON format: %w", err)
6266
}
6367

68+
// Unmarshal data into typed struct for agent name extraction
69+
var data aiagentconfig.Data
70+
if err := json.Unmarshal(envelope.Data, &data); err != nil {
71+
c.logger.Debug().Err(err).Msg("error decoding data field")
72+
return nil, fmt.Errorf("failed to unmarshal data: %w", err)
73+
}
74+
75+
// Validate using raw JSON to preserve unknown fields for strict schema validation
76+
var rawData any
77+
if err := json.Unmarshal(envelope.Data, &rawData); err != nil {
78+
return nil, fmt.Errorf("failed to unmarshal data for validation: %w", err)
79+
}
80+
6481
if err := schemavalidators.ValidateAIAgentConfig(rawData, schemavalidators.AIAgentConfigVersion0_1); err != nil {
6582
c.logger.Debug().Err(err).Msg("schema validation failed")
6683
return nil, fmt.Errorf("AI agent config validation failed: %w", err)
@@ -71,14 +88,9 @@ func (c *ChainloopAIAgentConfigCrafter) Craft(ctx context.Context, artifactPath
7188
return nil, err
7289
}
7390

74-
// Extract agent name from the validated JSON and surface it as an annotation
75-
var envelope struct {
76-
Agent struct {
77-
Name string `json:"name"`
78-
} `json:"agent"`
79-
}
80-
if err := json.Unmarshal(f, &envelope); err == nil && envelope.Agent.Name != "" {
81-
material.Annotations[annotationAIAgentName] = envelope.Agent.Name
91+
// Surface agent name as an annotation
92+
if data.Agent.Name != "" {
93+
material.Annotations[annotationAIAgentName] = data.Agent.Name
8294
}
8395

8496
return material, nil

0 commit comments

Comments
 (0)