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
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,10 @@
"reverted_by": {
"$ref": "#/definitions/commit_sha",
"description": "The commit that reverted this fix, established by reverse patch-id match against the artifact's known fixes"
},
"trace_sessions": {
"$ref": "#/definitions/trace_sessions",
"description": "Chainloop Trace session IDs (session_external_id) declared on the FIX commit via the Chainloop-Trace-Sessions git trailer — the AI coding session(s) that produced the fix. Recorded verbatim by the producer (no DB lookup); sorted + deduped."
}
}
},
Expand Down Expand Up @@ -838,6 +842,10 @@
"verified": {
"type": "boolean",
"description": "Proved to be an ancestor of the fix commit \u2014 NOT proved to be the true origin. Git can refute a commit that could not possibly have introduced the flaw; it cannot confirm that an ancestor is where the flaw actually entered. A consumer joining fingerprints on origin must require this."
},
"trace_sessions": {
"$ref": "#/definitions/trace_sessions",
"description": "Chainloop Trace session IDs (session_external_id) declared on this INTRODUCING commit via the Chainloop-Trace-Sessions git trailer \u2014 the AI coding session(s) that introduced the flaw. Recorded verbatim by the producer (no DB lookup); sorted + deduped."
}
}
},
Expand Down Expand Up @@ -909,6 +917,16 @@
"items": {
"$ref": "#/definitions/commit_sha"
}
},
"trace_sessions": {
"type": "array",
"description": "Chainloop Trace session IDs (session_external_id) declared on a commit via the Chainloop-Trace-Sessions git trailer, sorted + deduped. No pattern is enforced: session_external_id has no upstream format guarantee, so the IDs are treated as opaque strings. maxItems / maxLength are a defensive cap against a pathological or forged commit body inflating the artifact.",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 256
},
"maxItems": 64
}
}
}
84 changes: 84 additions & 0 deletions internal/schemavalidators/schemavalidators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,3 +647,87 @@ func TestValidateSecurityContextL0Class(t *testing.T) {
})
}
}

// TestValidateSecurityContextTraceSessions covers the trace_sessions array added
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
// to the fix commit (fingerprint) and to each introducing commit (commit_ref):
// the valid fixture already carries one on both, and the bounds (maxItems,
// per-item minLength/maxLength) reject a pathological or malformed trailer.
func TestValidateSecurityContextTraceSessions(t *testing.T) {
loadPayload := func(t *testing.T) map[string]any {
t.Helper()
f, err := os.ReadFile("./testdata/ai_security_context_valid.json")
require.NoError(t, err)

var payload map[string]any
require.NoError(t, json.Unmarshal(f, &payload))
return payload
}

// firstFingerprint returns fingerprints[0] so a case can mutate its
// trace_sessions (the fix commit) or its introduced_by[0] (an origin commit).
firstFingerprint := func(t *testing.T, payload map[string]any) map[string]any {
t.Helper()
fps, ok := payload["fingerprints"].([]any)
require.True(t, ok)
require.NotEmpty(t, fps)
fp, ok := fps[0].(map[string]any)
require.True(t, ok)
return fp
}

firstIntroducedBy := func(t *testing.T, fp map[string]any) map[string]any {
t.Helper()
origins, ok := fp["introduced_by"].([]any)
require.True(t, ok)
require.NotEmpty(t, origins)
origin, ok := origins[0].(map[string]any)
require.True(t, ok)
return origin
}

t.Run("the fixture carries trace_sessions on both commit roles", func(t *testing.T) {
payload := loadPayload(t)
fp := firstFingerprint(t, payload)
require.NotEmpty(t, fp["trace_sessions"], "fix commit should carry trace_sessions")
require.NotEmpty(t, firstIntroducedBy(t, fp)["trace_sessions"], "introducing commit should carry trace_sessions")
require.NoError(t, schemavalidators.ValidateSecurityContext(payload, ""))
})

t.Run("fix commit accepts several session ids", func(t *testing.T) {
payload := loadPayload(t)
firstFingerprint(t, payload)["trace_sessions"] = []any{
"8b012929-ba7f-4986-815c-2fbe692a9a98",
"an-opaque-non-uuid-session-id",
}
require.NoError(t, schemavalidators.ValidateSecurityContext(payload, ""))
})

overflow := make([]any, 65)
for i := range overflow {
overflow[i] = fmt.Sprintf("session-%02d", i)
}

rejected := []struct {
name string
value any
}{
{name: "more than maxItems session ids", value: overflow},
{name: "an empty-string session id", value: []any{""}},
{name: "a session id longer than maxLength", value: []any{strings.Repeat("a", 257)}},
{name: "a non-array trace_sessions", value: "not-an-array"},
}

for _, tc := range rejected {
t.Run("fix commit rejects "+tc.name, func(t *testing.T) {
payload := loadPayload(t)
firstFingerprint(t, payload)["trace_sessions"] = tc.value
require.Error(t, schemavalidators.ValidateSecurityContext(payload, ""))
})

t.Run("introducing commit rejects "+tc.name, func(t *testing.T) {
payload := loadPayload(t)
firstIntroducedBy(t, firstFingerprint(t, payload))["trace_sessions"] = tc.value
require.Error(t, schemavalidators.ValidateSecurityContext(payload, ""))
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,16 @@
"commit_sha": "c8533df53b0af4b731cb1036ec61aee10e35c67b",
"description": "GET /ls handler shell invocation",
"committed_at": "2026-08-19T19:56:46-03:00",
"verified": true
"verified": true,
"trace_sessions": [
"1f2e3d4c-5b6a-7980-a1b2-c3d4e5f60718"
]
}
],
"introduced_to_fixed_seconds": 102
"introduced_to_fixed_seconds": 102,
"trace_sessions": [
"8b012929-ba7f-4986-815c-2fbe692a9a98"
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ type CommitRef struct {
Description string `json:"description,omitempty"`
CommittedAt string `json:"committed_at,omitempty"`
Verified bool `json:"verified"`

// TraceSessions are the Chainloop Trace session IDs (session_external_id)
// declared on this introducing commit via the Chainloop-Trace-Sessions git
// trailer — the AI coding session(s) that introduced the flaw. Recorded
// verbatim by the producer with no DB lookup; sorted + deduped.
TraceSessions []string `json:"trace_sessions,omitempty"`
}

// Anchor is a byte-verifiable evidence span. A consumer can re-verify it
Expand Down Expand Up @@ -234,6 +240,12 @@ type Fingerprint struct {
// annotated rather than dropped.
Status string `json:"status,omitempty"`
RevertedBy string `json:"reverted_by,omitempty"`

// TraceSessions are the Chainloop Trace session IDs (session_external_id)
// declared on the fix commit via the Chainloop-Trace-Sessions git trailer —
// the AI coding session(s) that produced the fix. Recorded verbatim by the
// producer with no DB lookup; sorted + deduped.
TraceSessions []string `json:"trace_sessions,omitempty"`
}

// Data is the AI security context payload: the object the producer writes under
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,10 +544,16 @@
"commit_sha": "d4bdebd22566dd9c871036eb39343033acd8c6bc",
"description": "fixed-precision multipleOf conversion",
"committed_at": "2025-05-09T02:19:30+02:00",
"verified": true
"verified": true,
"trace_sessions": [
"1f2e3d4c-5b6a-7980-a1b2-c3d4e5f60718"
]
}
],
"introduced_to_fixed_seconds": 40385717
"introduced_to_fixed_seconds": 40385717,
"trace_sessions": [
"8b012929-ba7f-4986-815c-2fbe692a9a98"
]
},
{
"id": "fp_eda80e2676",
Expand Down
Loading