Skip to content

Commit c8400fc

Browse files
authored
feat(attestation): add missing fields to aicodingsession upstream types (#3008)
Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
1 parent 8f7b02d commit c8400fc

4 files changed

Lines changed: 341 additions & 12 deletions

File tree

internal/schemavalidators/internal_schemas/aicodingsession/ai-coding-session-0.1.schema.json

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,101 @@
6969
},
7070
"code_changes": {
7171
"type": "object",
72-
"description": "Summary of code modifications"
72+
"description": "Summary of code modifications",
73+
"properties": {
74+
"files_modified": {
75+
"type": "integer",
76+
"description": "Number of files modified"
77+
},
78+
"files_created": {
79+
"type": "integer",
80+
"description": "Number of files created"
81+
},
82+
"files_deleted": {
83+
"type": "integer",
84+
"description": "Number of files deleted"
85+
},
86+
"lines_added": {
87+
"type": "integer",
88+
"description": "Total lines added"
89+
},
90+
"lines_removed": {
91+
"type": "integer",
92+
"description": "Total lines removed"
93+
},
94+
"ai_lines_added": {
95+
"type": "integer",
96+
"description": "Lines added attributed to AI"
97+
},
98+
"ai_lines_removed": {
99+
"type": "integer",
100+
"description": "Lines removed attributed to AI"
101+
},
102+
"human_lines_added": {
103+
"type": "integer",
104+
"description": "Lines added attributed to human"
105+
},
106+
"human_lines_removed": {
107+
"type": "integer",
108+
"description": "Lines removed attributed to human"
109+
},
110+
"files": {
111+
"type": "array",
112+
"description": "Per-file change details",
113+
"items": {
114+
"type": "object",
115+
"required": ["path", "status"],
116+
"properties": {
117+
"path": {
118+
"type": "string",
119+
"description": "File path"
120+
},
121+
"status": {
122+
"type": "string",
123+
"description": "Change status (e.g. modified, created, deleted)"
124+
},
125+
"lines_added": {
126+
"type": "integer",
127+
"description": "Lines added in this file"
128+
},
129+
"lines_removed": {
130+
"type": "integer",
131+
"description": "Lines removed in this file"
132+
},
133+
"attribution": {
134+
"type": "string",
135+
"description": "Change attribution: ai or human"
136+
},
137+
"line_ranges": {
138+
"type": "array",
139+
"description": "Contiguous line ranges",
140+
"items": {
141+
"type": "object",
142+
"required": ["start", "end"],
143+
"properties": {
144+
"start": {
145+
"type": "integer",
146+
"description": "Start line number"
147+
},
148+
"end": {
149+
"type": "integer",
150+
"description": "End line number"
151+
}
152+
},
153+
"additionalProperties": false
154+
}
155+
},
156+
"session_ids": {
157+
"type": "array",
158+
"description": "Session IDs associated with changes in this file",
159+
"items": {
160+
"type": "string"
161+
}
162+
}
163+
}
164+
}
165+
}
166+
}
73167
},
74168
"model": {
75169
"type": "object",
@@ -89,9 +183,37 @@
89183
},
90184
"subagents": {
91185
"type": "array",
92-
"description": "Subagent information (open schema)",
186+
"description": "Subagent information",
93187
"items": {
94-
"type": "object"
188+
"type": "object",
189+
"properties": {
190+
"id": {
191+
"type": "string",
192+
"description": "Unique subagent identifier"
193+
},
194+
"type": {
195+
"type": "string",
196+
"description": "Subagent type"
197+
},
198+
"description": {
199+
"type": "string",
200+
"description": "Subagent description"
201+
},
202+
"tokens": {
203+
"type": "object",
204+
"description": "Token usage for this subagent",
205+
"properties": {
206+
"input": {
207+
"type": "integer",
208+
"description": "Input tokens consumed"
209+
},
210+
"output": {
211+
"type": "integer",
212+
"description": "Output tokens produced"
213+
}
214+
}
215+
}
216+
}
95217
}
96218
},
97219
"raw_session": {

pkg/attestation/crafter/materials/aicodingsession/aicodingsession.go

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,35 @@ type GitContext struct {
5050
CommitCount int `json:"commit_count,omitempty"`
5151
}
5252

53+
// LineRange represents a contiguous range of lines.
54+
type LineRange struct {
55+
Start int `json:"start"`
56+
End int `json:"end"`
57+
}
58+
5359
// FileChange represents a single file modification in the session.
5460
type FileChange struct {
55-
Path string `json:"path"`
56-
Status string `json:"status"`
61+
Path string `json:"path"`
62+
Status string `json:"status"`
63+
LinesAdded int `json:"lines_added,omitempty"`
64+
LinesRemoved int `json:"lines_removed,omitempty"`
65+
Attribution string `json:"attribution,omitempty"`
66+
LineRanges []LineRange `json:"line_ranges,omitempty"`
67+
SessionIDs []string `json:"session_ids,omitempty"`
5768
}
5869

5970
// CodeChanges summarizes code modifications made during the session.
6071
type CodeChanges struct {
61-
FilesModified int `json:"files_modified,omitempty"`
62-
FilesCreated int `json:"files_created,omitempty"`
63-
FilesDeleted int `json:"files_deleted,omitempty"`
64-
LinesAdded int `json:"lines_added,omitempty"`
65-
LinesRemoved int `json:"lines_removed,omitempty"`
66-
Files []FileChange `json:"files,omitempty"`
72+
FilesModified int `json:"files_modified,omitempty"`
73+
FilesCreated int `json:"files_created,omitempty"`
74+
FilesDeleted int `json:"files_deleted,omitempty"`
75+
LinesAdded int `json:"lines_added,omitempty"`
76+
LinesRemoved int `json:"lines_removed,omitempty"`
77+
AILinesAdded int `json:"ai_lines_added,omitempty"`
78+
AILinesRemoved int `json:"ai_lines_removed,omitempty"`
79+
HumanLinesAdded int `json:"human_lines_added,omitempty"`
80+
HumanLinesRemoved int `json:"human_lines_removed,omitempty"`
81+
Files []FileChange `json:"files,omitempty"`
6782
}
6883

6984
// Model holds information about the AI models used in the session.
@@ -95,6 +110,20 @@ type ToolsUsed struct {
95110
TotalInvocations int `json:"total_invocations,omitempty"`
96111
}
97112

113+
// SubagentTokens holds token usage for a subagent.
114+
type SubagentTokens struct {
115+
Input int `json:"input"`
116+
Output int `json:"output"`
117+
}
118+
119+
// Subagent describes a spawned subagent within the session.
120+
type Subagent struct {
121+
ID string `json:"id"`
122+
Type string `json:"type"`
123+
Description string `json:"description"`
124+
Tokens SubagentTokens `json:"tokens"`
125+
}
126+
98127
// Conversation holds message count statistics.
99128
type Conversation struct {
100129
TotalMessages int `json:"total_messages,omitempty"`
@@ -113,7 +142,7 @@ type Data struct {
113142
Usage *Usage `json:"usage,omitempty"`
114143
ToolsUsed *ToolsUsed `json:"tools_used,omitempty"`
115144
Conversation *Conversation `json:"conversation,omitempty"`
116-
Subagents []json.RawMessage `json:"subagents,omitempty"`
145+
Subagents []Subagent `json:"subagents,omitempty"`
117146
RawSession map[string][]json.RawMessage `json:"raw_session,omitempty"`
118147
Warnings []string `json:"warnings,omitempty"`
119148
}

pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,68 @@ func TestChainloopAICodingSessionCrafter_RejectsExtraFields(t *testing.T) {
226226
assert.Contains(t, err.Error(), "AI coding session validation failed")
227227
}
228228

229+
func TestChainloopAICodingSessionCrafter_RealWorldEvidence(t *testing.T) {
230+
// Load real-world evidence that uses the new attribution, line range, and subagent fields.
231+
raw, err := os.ReadFile("./testdata/ai-coding-session-with-attribution.json")
232+
require.NoError(t, err)
233+
234+
var evidence aicodingsession.Evidence
235+
require.NoError(t, json.Unmarshal(raw, &evidence))
236+
237+
data := evidence.Data
238+
239+
// Top-level identifiers
240+
assert.Equal(t, aicodingsession.EvidenceID, evidence.ID)
241+
assert.Equal(t, aicodingsession.EvidenceSchemaURL, evidence.Schema)
242+
assert.Equal(t, "v1", data.SchemaVersion)
243+
244+
// Agent
245+
assert.Equal(t, "claude-code", data.Agent.Name)
246+
assert.Equal(t, "2.1.92", data.Agent.Version)
247+
248+
// Session
249+
assert.Equal(t, "1dea3f43-4c8f-4625-8732-349613261162", data.Session.ID)
250+
assert.Equal(t, 14, data.Session.DurationSeconds)
251+
252+
// Code changes — AI/human attribution
253+
require.NotNil(t, data.CodeChanges)
254+
assert.Equal(t, 11, data.CodeChanges.LinesAdded)
255+
assert.Equal(t, 8, data.CodeChanges.AILinesAdded)
256+
assert.Equal(t, 0, data.CodeChanges.AILinesRemoved)
257+
assert.Equal(t, 3, data.CodeChanges.HumanLinesAdded)
258+
assert.Equal(t, 0, data.CodeChanges.HumanLinesRemoved)
259+
260+
// File-level fields
261+
require.Len(t, data.CodeChanges.Files, 2)
262+
263+
goMod := data.CodeChanges.Files[0]
264+
assert.Equal(t, "go.mod", goMod.Path)
265+
assert.Equal(t, "human", goMod.Attribution)
266+
assert.Equal(t, 3, goMod.LinesAdded)
267+
268+
mainGo := data.CodeChanges.Files[1]
269+
assert.Equal(t, "main.go", mainGo.Path)
270+
assert.Equal(t, "ai", mainGo.Attribution)
271+
assert.Equal(t, 8, mainGo.LinesAdded)
272+
require.Len(t, mainGo.LineRanges, 1)
273+
assert.Equal(t, 6, mainGo.LineRanges[0].Start)
274+
assert.Equal(t, 6, mainGo.LineRanges[0].End)
275+
require.Len(t, mainGo.SessionIDs, 1)
276+
assert.Equal(t, "1dea3f43-4c8f-4625-8732-349613261162", mainGo.SessionIDs[0])
277+
278+
// Usage
279+
require.NotNil(t, data.Usage)
280+
assert.InDelta(t, 0.1917, data.Usage.EstimatedCostUSD, 0.0001)
281+
282+
// Schema validation should pass
283+
dataBytes, err := json.Marshal(data)
284+
require.NoError(t, err)
285+
286+
var rawData any
287+
require.NoError(t, json.Unmarshal(dataBytes, &rawData))
288+
require.NoError(t, schemavalidators.ValidateAICodingSession(rawData, schemavalidators.AICodingSessionVersion0_1))
289+
}
290+
229291
func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) {
230292
testCases := []struct {
231293
name string
@@ -241,6 +303,13 @@ func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) {
241303
expectedModel: "claude-opus-4-6",
242304
modelPresent: true,
243305
},
306+
{
307+
name: "session with AI attribution fields",
308+
filePath: "./testdata/ai-coding-session-with-attribution.json",
309+
expectedAgentName: "claude-code",
310+
expectedModel: "claude-opus-4-6",
311+
modelPresent: true,
312+
},
244313
{
245314
name: "minimal session without model",
246315
filePath: "./testdata/ai-coding-session-minimal.json",

0 commit comments

Comments
 (0)