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
9 changes: 7 additions & 2 deletions internal/aiagentconfig/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ import (
// 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, discovered []DiscoveredFile, agentName string, gitCtx *GitContext) (*Data, error) {
// capturedAt is the timestamp to record in the output; pass time.Time{} to use the current time.
func Build(basePath string, discovered []DiscoveredFile, agentName string, gitCtx *GitContext, capturedAt time.Time) (*Data, error) {
if capturedAt.IsZero() {
capturedAt = time.Now().UTC()
}

// Resolve basePath to its real path so symlink comparisons are reliable
realRoot, err := filepath.EvalSymlinks(basePath)
if err != nil {
Expand Down Expand Up @@ -82,7 +87,7 @@ func Build(basePath string, discovered []DiscoveredFile, agentName string, gitCt
data := Data{
Agent: Agent{Name: agentName},
ConfigHash: computeCombinedHash(hashes),
CapturedAt: time.Now().UTC().Format(time.RFC3339),
CapturedAt: capturedAt.Format(time.RFC3339),
GitContext: gitCtx,
ConfigFiles: configFiles,
}
Expand Down
19 changes: 10 additions & 9 deletions internal/aiagentconfig/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"sort"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -50,7 +51,7 @@ func TestBuild(t *testing.T) {
data, err := Build(rootDir, []DiscoveredFile{
{Path: "CLAUDE.md", Kind: ConfigFileKindInstruction},
{Path: ".claude/settings.json", Kind: ConfigFileKindConfiguration},
}, "claude", gitCtx)
}, "claude", gitCtx, time.Time{})
require.NoError(t, err)

assert.Equal(t, "claude", data.Agent.Name)
Expand Down Expand Up @@ -97,7 +98,7 @@ func TestBuildWithCursorAgent(t *testing.T) {

data, err := Build(rootDir, []DiscoveredFile{
{Path: ".cursor/rules/coding.md", Kind: ConfigFileKindInstruction},
}, "cursor", nil)
}, "cursor", nil, time.Time{})
require.NoError(t, err)

assert.Equal(t, "cursor", data.Agent.Name)
Expand All @@ -112,7 +113,7 @@ func TestBuildWithoutGitContext(t *testing.T) {

data, err := Build(rootDir, []DiscoveredFile{
{Path: "CLAUDE.md", Kind: ConfigFileKindInstruction},
}, "claude", nil)
}, "claude", nil, time.Time{})
require.NoError(t, err)

assert.Nil(t, data.GitContext)
Expand All @@ -125,7 +126,7 @@ func TestBuildJSONFormat(t *testing.T) {

data, err := Build(rootDir, []DiscoveredFile{
{Path: "CLAUDE.md", Kind: ConfigFileKindInstruction},
}, "claude", nil)
}, "claude", nil, time.Time{})
require.NoError(t, err)

// Verify it marshals to valid JSON with top-level fields (no envelope)
Expand Down Expand Up @@ -160,7 +161,7 @@ func TestBuildRejectsSymlinksEscapingRoot(t *testing.T) {

_, err := Build(rootDir, []DiscoveredFile{
{Path: "CLAUDE.md", Kind: ConfigFileKindInstruction},
}, "claude", nil)
}, "claude", nil, time.Time{})
require.Error(t, err)
assert.Contains(t, err.Error(), "path escapes root directory via symlink")
}
Expand All @@ -179,15 +180,15 @@ func TestBuildRejectsSymlinkedParentDir(t *testing.T) {

_, err := Build(rootDir, []DiscoveredFile{
{Path: ".claude/settings.json", Kind: ConfigFileKindConfiguration},
}, "claude", nil)
}, "claude", nil, time.Time{})
require.Error(t, err)
assert.Contains(t, err.Error(), "path escapes root directory via symlink")
}

func TestBuildEmptyFileList(t *testing.T) {
rootDir := t.TempDir()

data, err := Build(rootDir, []DiscoveredFile{}, "claude", nil)
data, err := Build(rootDir, []DiscoveredFile{}, "claude", nil, time.Time{})
require.NoError(t, err)
assert.Empty(t, data.ConfigFiles)
assert.NotEmpty(t, data.ConfigHash)
Expand All @@ -197,7 +198,7 @@ func TestBuildEmptyFileList(t *testing.T) {
func TestBuildNilFileList(t *testing.T) {
rootDir := t.TempDir()

data, err := Build(rootDir, nil, "claude", nil)
data, err := Build(rootDir, nil, "claude", nil, time.Time{})
require.NoError(t, err)
assert.Empty(t, data.ConfigFiles)
}
Expand All @@ -208,7 +209,7 @@ func TestBuildAllowsRegularFilesInRoot(t *testing.T) {

data, err := Build(rootDir, []DiscoveredFile{
{Path: "CLAUDE.md", Kind: ConfigFileKindInstruction},
}, "claude", nil)
}, "claude", nil, time.Time{})
require.NoError(t, err)
assert.Len(t, data.ConfigFiles, 1)
}
2 changes: 1 addition & 1 deletion pkg/attestation/crafter/collector_aiagentconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (c *AIAgentConfigCollector) uploadAgentConfig(
ctx context.Context, cr *Crafter, attestationID string,
casBackend *casclient.CASBackend, agentName string, files []aiagentconfig.DiscoveredFile, gitCtx *aiagentconfig.GitContext,
) error {
data, err := aiagentconfig.Build(cr.WorkingDir(), files, agentName, gitCtx)
data, err := aiagentconfig.Build(cr.WorkingDir(), files, agentName, gitCtx, cr.CraftingState.GetAttestation().GetInitializedAt().AsTime())
if err != nil {
return fmt.Errorf("building AI agent config for %s: %w", agentName, err)
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/attestation/crafter/crafter.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ func (c *Crafter) WorkingDir() string {
return c.workingDir
}

// RegisterCollectors appends collectors to be run during attestation init.
// RegisterCollectors resets collectors to be run during attestation init.
func (c *Crafter) RegisterCollectors(collectors ...Collector) {
c.collectors = append(c.collectors, collectors...)
c.collectors = collectors
}

// RunCollectors executes all registered collectors best-effort.
Expand All @@ -158,7 +158,7 @@ func (c *Crafter) RunCollectors(ctx context.Context, attestationID string, casBa
// NOTE: workaround for old servers that don't return digests in Save responses.
// not returning digest makes digests stale, and state save failing with conflict errors
// https://github.com/chainloop-dev/chainloop/issues/2908
// this conditition will not apply to new servers that return digests in Save responses.
// this condition will not apply to new servers that return digests in Save responses.
if c.CraftingState.UpdateCheckSum != digestBeforeCollectors {
if err := c.LoadCraftingState(ctx, attestationID); err != nil {
c.Logger.Warn().Err(err).Msg("failed to reload crafting state after running collectors")
Expand Down
Loading