From cac21992998dc02da63a9e21599e0f2fc4f9b6b6 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 25 Mar 2026 20:49:22 +0100 Subject: [PATCH 1/5] feat: add CHAINLOOP_AI_CODING_SESSION material type Add a new material type for capturing AI coding session telemetry including agent info, session timing, code changes, token usage, model info, and raw conversation data. Follows the same chainloop custom type wrapper pattern used by CHAINLOOP_AI_AGENT_CONFIG. Signed-off-by: Jose I. Paris --- .../workflowcontract/v1/crafting_schema.ts | 7 + ...on.v1.Attestation.Material.jsonschema.json | 6 +- ...tation.v1.Attestation.Material.schema.json | 6 +- ...tation.v1.PolicyEvaluation.jsonschema.json | 3 +- ...ttestation.v1.PolicyEvaluation.schema.json | 3 +- ...v1.CraftingSchema.Material.jsonschema.json | 3 +- ...act.v1.CraftingSchema.Material.schema.json | 3 +- ...ct.v1.PolicyGroup.Material.jsonschema.json | 3 +- ...ntract.v1.PolicyGroup.Material.schema.json | 3 +- ...flowcontract.v1.PolicySpec.jsonschema.json | 3 +- ...workflowcontract.v1.PolicySpec.schema.json | 3 +- ...owcontract.v1.PolicySpecV2.jsonschema.json | 3 +- ...rkflowcontract.v1.PolicySpecV2.schema.json | 3 +- .../workflowcontract/v1/crafting_schema.pb.go | 13 +- .../workflowcontract/v1/crafting_schema.proto | 2 + .../v1/crafting_schema_validations.go | 1 + internal/aicodingsession/aicodingsession.go | 135 +++++++++ .../ai-coding-session-0.1.schema.json | 113 +++++++ internal/schemavalidators/schemavalidators.go | 68 ++++- .../materials/chainloop_ai_coding_session.go | 102 +++++++ .../chainloop_ai_coding_session_test.go | 284 ++++++++++++++++++ .../crafter/materials/materials.go | 2 + .../testdata/ai-coding-session-minimal.json | 15 + .../materials/testdata/ai-coding-session.json | 77 +++++ 24 files changed, 827 insertions(+), 34 deletions(-) create mode 100644 internal/aicodingsession/aicodingsession.go create mode 100644 internal/schemavalidators/internal_schemas/aicodingsession/ai-coding-session-0.1.schema.json create mode 100644 pkg/attestation/crafter/materials/chainloop_ai_coding_session.go create mode 100644 pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go create mode 100644 pkg/attestation/crafter/materials/testdata/ai-coding-session-minimal.json create mode 100644 pkg/attestation/crafter/materials/testdata/ai-coding-session.json diff --git a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts index 04b8e1d2d..0165cefcf 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -255,6 +255,8 @@ export enum CraftingSchema_Material_MaterialType { GITLEAKS_JSON = 27, /** CHAINLOOP_AI_AGENT_CONFIG - AI agent configuration collected automatically during attestation */ CHAINLOOP_AI_AGENT_CONFIG = 28, + /** CHAINLOOP_AI_CODING_SESSION - AI coding session telemetry collected during attestation */ + CHAINLOOP_AI_CODING_SESSION = 29, UNRECOGNIZED = -1, } @@ -347,6 +349,9 @@ export function craftingSchema_Material_MaterialTypeFromJSON(object: any): Craft case 28: case "CHAINLOOP_AI_AGENT_CONFIG": return CraftingSchema_Material_MaterialType.CHAINLOOP_AI_AGENT_CONFIG; + case 29: + case "CHAINLOOP_AI_CODING_SESSION": + return CraftingSchema_Material_MaterialType.CHAINLOOP_AI_CODING_SESSION; case -1: case "UNRECOGNIZED": default: @@ -414,6 +419,8 @@ export function craftingSchema_Material_MaterialTypeToJSON(object: CraftingSchem return "GITLEAKS_JSON"; case CraftingSchema_Material_MaterialType.CHAINLOOP_AI_AGENT_CONFIG: return "CHAINLOOP_AI_AGENT_CONFIG"; + case CraftingSchema_Material_MaterialType.CHAINLOOP_AI_CODING_SESSION: + return "CHAINLOOP_AI_CODING_SESSION"; case CraftingSchema_Material_MaterialType.UNRECOGNIZED: default: return "UNRECOGNIZED"; diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json index b90174e57..ea559179f 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json @@ -45,7 +45,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" @@ -126,7 +127,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json index b8d0018ff..08a5a4500 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json @@ -45,7 +45,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" @@ -126,7 +127,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json index 9bbd3e3db..208de81ec 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json @@ -142,7 +142,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json index f1ae2bc9d..36f7559a8 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json @@ -142,7 +142,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json index 08b6a62bf..fc3243a9d 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json @@ -62,7 +62,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json index 67f66d436..a0da8565d 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json @@ -62,7 +62,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json index 438d0f353..688363e15 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json @@ -50,7 +50,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json index 47fff7fed..504538604 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json @@ -50,7 +50,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json index 8b4c4b97f..a7f9c40dd 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json @@ -64,7 +64,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json index 17484e2d3..2f5fc7af1 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json @@ -64,7 +64,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json index 3d2db99b8..3e128da17 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json @@ -85,7 +85,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json index 3f8383f01..a502e9418 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json @@ -85,7 +85,8 @@ "CHAINLOOP_RUNNER_CONTEXT", "CHAINLOOP_PR_INFO", "GITLEAKS_JSON", - "CHAINLOOP_AI_AGENT_CONFIG" + "CHAINLOOP_AI_AGENT_CONFIG", + "CHAINLOOP_AI_CODING_SESSION" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index ff6c3c819..41dfac138 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -208,6 +208,8 @@ const ( CraftingSchema_Material_GITLEAKS_JSON CraftingSchema_Material_MaterialType = 27 // AI agent configuration collected automatically during attestation CraftingSchema_Material_CHAINLOOP_AI_AGENT_CONFIG CraftingSchema_Material_MaterialType = 28 + // AI coding session telemetry collected during attestation + CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION CraftingSchema_Material_MaterialType = 29 ) // Enum value maps for CraftingSchema_Material_MaterialType. @@ -242,6 +244,7 @@ var ( 26: "CHAINLOOP_PR_INFO", 27: "GITLEAKS_JSON", 28: "CHAINLOOP_AI_AGENT_CONFIG", + 29: "CHAINLOOP_AI_CODING_SESSION", } CraftingSchema_Material_MaterialType_value = map[string]int32{ "MATERIAL_TYPE_UNSPECIFIED": 0, @@ -273,6 +276,7 @@ var ( "CHAINLOOP_PR_INFO": 26, "GITLEAKS_JSON": 27, "CHAINLOOP_AI_AGENT_CONFIG": 28, + "CHAINLOOP_AI_CODING_SESSION": 29, } ) @@ -1919,7 +1923,7 @@ var File_workflowcontract_v1_crafting_schema_proto protoreflect.FileDescriptor const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\n" + - ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\xc9\x0e\n" + + ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\xea\x0e\n" + "\x0eCraftingSchema\x122\n" + "\x0eschema_version\x18\x01 \x01(\tB\v\xbaH\x06r\x04\n" + "\x02v1\x18\x01R\rschemaVersion\x12N\n" + @@ -1941,7 +1945,7 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x0eCIRCLECI_BUILD\x10\x05\x12\x13\n" + "\x0fDAGGER_PIPELINE\x10\x06\x12\x15\n" + "\x11TEAMCITY_PIPELINE\x10\a\x12\x13\n" + - "\x0fTEKTON_PIPELINE\x10\b:\x02\x18\x01\x1a\xab\b\n" + + "\x0fTEKTON_PIPELINE\x10\b:\x02\x18\x01\x1a\xcc\b\n" + "\bMaterial\x12[\n" + "\x04type\x18\x01 \x01(\x0e29.workflowcontract.v1.CraftingSchema.Material.MaterialTypeB\f\xbaH\a\x82\x01\x04\x10\x01 \x00\x18\x01R\x04type\x12\x99\x01\n" + "\x04name\x18\x02 \x01(\tB\x84\x01\xbaH\x7f\xba\x01|\n" + @@ -1950,7 +1954,7 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x06output\x18\x04 \x01(\bB\x02\x18\x01R\x06output\x12E\n" + "\vannotations\x18\x05 \x03(\v2\x1f.workflowcontract.v1.AnnotationB\x02\x18\x01R\vannotations\x12\x1f\n" + "\vskip_upload\x18\x06 \x01(\bR\n" + - "skipUpload\"\xfd\x04\n" + + "skipUpload\"\x9e\x05\n" + "\fMaterialType\x12\x1d\n" + "\x19MATERIAL_TYPE_UNSPECIFIED\x10\x00\x12\n" + "\n" + @@ -1984,7 +1988,8 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x18CHAINLOOP_RUNNER_CONTEXT\x10\x19\x12\x15\n" + "\x11CHAINLOOP_PR_INFO\x10\x1a\x12\x11\n" + "\rGITLEAKS_JSON\x10\x1b\x12\x1d\n" + - "\x19CHAINLOOP_AI_AGENT_CONFIG\x10\x1c:\x02\x18\x01:\x02\x18\x01\"\xfb\x01\n" + + "\x19CHAINLOOP_AI_AGENT_CONFIG\x10\x1c\x12\x1f\n" + + "\x1bCHAINLOOP_AI_CODING_SESSION\x10\x1d:\x02\x18\x01:\x02\x18\x01\"\xfb\x01\n" + "\x10CraftingSchemaV2\x128\n" + "\vapi_version\x18\x01 \x01(\tB\x17\xbaH\x14r\x12\n" + "\x10chainloop.dev/v1R\n" + diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index 2f54c33ed..b92e4d2a7 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -151,6 +151,8 @@ message CraftingSchema { GITLEAKS_JSON = 27; // AI agent configuration collected automatically during attestation CHAINLOOP_AI_AGENT_CONFIG = 28; + // AI coding session telemetry collected during attestation + CHAINLOOP_AI_CODING_SESSION = 29; } } } diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go index 138f55189..23b362c6c 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema_validations.go @@ -50,6 +50,7 @@ var CraftingMaterialInValidationOrder = []CraftingSchema_Material_MaterialType{ CraftingSchema_Material_SLSA_PROVENANCE, CraftingSchema_Material_CHAINLOOP_RUNNER_CONTEXT, CraftingSchema_Material_CHAINLOOP_AI_AGENT_CONFIG, + CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION, CraftingSchema_Material_ATTESTATION, CraftingSchema_Material_CONTAINER_IMAGE, CraftingSchema_Material_ARTIFACT, diff --git a/internal/aicodingsession/aicodingsession.go b/internal/aicodingsession/aicodingsession.go new file mode 100644 index 000000000..f614668d7 --- /dev/null +++ b/internal/aicodingsession/aicodingsession.go @@ -0,0 +1,135 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aicodingsession + +import "encoding/json" + +const ( + // EvidenceID is the identifier for the AI coding session material type + EvidenceID = "CHAINLOOP_AI_CODING_SESSION" + // EvidenceSchemaURL is the URL to the JSON schema for AI coding session + EvidenceSchemaURL = "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json" +) + +// Agent identifies the AI agent provider. +type Agent struct { + Name string `json:"name"` + Version string `json:"version,omitempty"` +} + +// Session holds timing and identity information for the coding session. +type Session struct { + ID string `json:"id"` + Slug string `json:"slug,omitempty"` + StartedAt string `json:"started_at"` + EndedAt string `json:"ended_at,omitempty"` + DurationSeconds int `json:"duration_seconds"` +} + +// GitContext holds repository and commit information at capture time. +type GitContext struct { + Repository string `json:"repository,omitempty"` + Branch string `json:"branch,omitempty"` + WorkDir string `json:"work_dir,omitempty"` + CommitStart string `json:"commit_start,omitempty"` + CommitEnd string `json:"commit_end,omitempty"` + Commits []string `json:"commits,omitempty"` + CommitCount int `json:"commit_count,omitempty"` +} + +// FileChange represents a single file modification in the session. +type FileChange struct { + Path string `json:"path"` + Status string `json:"status"` +} + +// CodeChanges summarizes code modifications made during the session. +type CodeChanges struct { + FilesModified int `json:"files_modified,omitempty"` + FilesCreated int `json:"files_created,omitempty"` + FilesDeleted int `json:"files_deleted,omitempty"` + LinesAdded int `json:"lines_added,omitempty"` + LinesRemoved int `json:"lines_removed,omitempty"` + Files []FileChange `json:"files,omitempty"` +} + +// Model holds information about the AI models used in the session. +type Model struct { + Primary string `json:"primary,omitempty"` + Provider string `json:"provider,omitempty"` + ModelsUsed []string `json:"models_used,omitempty"` +} + +// Usage holds token usage and cost information. +type Usage struct { + InputTokens int `json:"input_tokens,omitempty"` + OutputTokens int `json:"output_tokens,omitempty"` + TotalTokens int `json:"total_tokens,omitempty"` + CacheReadInputTokens int `json:"cache_read_input_tokens,omitempty"` + CacheCreationInputTokens int `json:"cache_creation_input_tokens,omitempty"` + EstimatedCostUSD float64 `json:"estimated_cost_usd,omitempty"` +} + +// ToolSummary represents usage statistics for a single tool. +type ToolSummary struct { + ToolName string `json:"tool_name"` + InvocationCount int `json:"invocation_count"` +} + +// ToolsUsed summarizes tool usage during the session. +type ToolsUsed struct { + Summary []ToolSummary `json:"summary,omitempty"` + TotalInvocations int `json:"total_invocations,omitempty"` +} + +// Conversation holds message count statistics. +type Conversation struct { + TotalMessages int `json:"total_messages,omitempty"` + UserMessages int `json:"user_messages,omitempty"` + AssistantMessages int `json:"assistant_messages,omitempty"` +} + +// Data is the AI coding session payload. +type Data struct { + SchemaVersion string `json:"schema_version"` + Agent Agent `json:"agent"` + Session Session `json:"session"` + GitContext *GitContext `json:"git_context,omitempty"` + CodeChanges *CodeChanges `json:"code_changes,omitempty"` + Model *Model `json:"model,omitempty"` + Usage *Usage `json:"usage,omitempty"` + ToolsUsed *ToolsUsed `json:"tools_used,omitempty"` + Conversation *Conversation `json:"conversation,omitempty"` + Subagents []json.RawMessage `json:"subagents,omitempty"` + RawSession map[string][]json.RawMessage `json:"raw_session,omitempty"` + Warnings []string `json:"warnings,omitempty"` +} + +// Evidence represents the complete evidence structure for AI coding session. +type Evidence struct { + ID string `json:"chainloop.material.evidence.id"` + Schema string `json:"schema"` + Data Data `json:"data"` +} + +// NewEvidence creates a new Evidence instance. +func NewEvidence(data Data) *Evidence { + return &Evidence{ + ID: EvidenceID, + Schema: EvidenceSchemaURL, + Data: data, + } +} diff --git a/internal/schemavalidators/internal_schemas/aicodingsession/ai-coding-session-0.1.schema.json b/internal/schemavalidators/internal_schemas/aicodingsession/ai-coding-session-0.1.schema.json new file mode 100644 index 000000000..9aacfab58 --- /dev/null +++ b/internal/schemavalidators/internal_schemas/aicodingsession/ai-coding-session-0.1.schema.json @@ -0,0 +1,113 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", + "type": "object", + "title": "AI Coding Session", + "description": "Schema for AI coding session telemetry data collected during attestation", + "required": [ + "schema_version", + "agent", + "session" + ], + "properties": { + "schema_version": { + "type": "string", + "minLength": 1, + "description": "Schema version identifier" + }, + "agent": { + "type": "object", + "description": "AI agent provider information", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "Agent provider name" + }, + "version": { + "type": "string", + "description": "Agent version" + } + }, + "additionalProperties": false + }, + "session": { + "type": "object", + "description": "Session timing and identity", + "required": ["id", "started_at", "duration_seconds"], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "description": "Unique session identifier" + }, + "slug": { + "type": "string", + "description": "Human-readable session slug" + }, + "started_at": { + "type": "string", + "minLength": 1, + "description": "ISO 8601 timestamp when the session started" + }, + "ended_at": { + "type": "string", + "description": "ISO 8601 timestamp when the session ended" + }, + "duration_seconds": { + "type": "integer", + "minimum": 0, + "description": "Total session duration in seconds" + } + }, + "additionalProperties": false + }, + "git_context": { + "type": "object", + "description": "Repository and commit information" + }, + "code_changes": { + "type": "object", + "description": "Summary of code modifications" + }, + "model": { + "type": "object", + "description": "AI model information" + }, + "usage": { + "type": "object", + "description": "Token usage and cost information" + }, + "tools_used": { + "type": "object", + "description": "Tool usage statistics" + }, + "conversation": { + "type": "object", + "description": "Message count statistics" + }, + "subagents": { + "type": "array", + "description": "Subagent information (open schema)", + "items": { + "type": "object" + } + }, + "raw_session": { + "type": "object", + "description": "Raw session conversation data keyed by thread name", + "additionalProperties": { + "type": "array" + } + }, + "warnings": { + "type": "array", + "description": "Warnings generated during session processing", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/internal/schemavalidators/schemavalidators.go b/internal/schemavalidators/schemavalidators.go index 594f9d4b5..a4ee43776 100644 --- a/internal/schemavalidators/schemavalidators.go +++ b/internal/schemavalidators/schemavalidators.go @@ -43,6 +43,9 @@ type PRInfoVersion string // AIAgentConfigVersion represents the version of AI Agent Config schema. type AIAgentConfigVersion string +// AICodingSessionVersion represents the version of AI Coding Session schema. +type AICodingSessionVersion string + const ( // RunnerContextVersion0_1 represents Runner Context version 0.1 schema. RunnerContextVersion0_1 RunnerContextVersion = "0.1" @@ -64,6 +67,8 @@ const ( CSAFVersion2_1 CSAFVersion = "2.1" // AIAgentConfigVersion0_1 represents AI Agent Config version 0.1 schema. AIAgentConfigVersion0_1 AIAgentConfigVersion = "0.1" + // AICodingSessionVersion0_1 represents AI Coding Session version 0.1 schema. + AICodingSessionVersion0_1 AICodingSessionVersion = "0.1" ) var ( @@ -108,6 +113,10 @@ var ( // AI Agent Config schemas //go:embed internal_schemas/aiagentconfig/ai-agent-config-0.1.schema.json aiAgentConfigSpecVersion0_1 string + + // AI Coding Session schemas + //go:embed internal_schemas/aicodingsession/ai-coding-session-0.1.schema.json + aiCodingSessionSpecVersion0_1 string ) // schemaURLMapping maps the schema URL to the schema content. This is used to compile the schema validators @@ -115,22 +124,23 @@ var ( // The keys are the URLs of the schemas and the values are the schema content that can be found in the embedded // files. var schemaURLMapping = map[string]string{ - "http://cyclonedx.org/schema/jsf-0.82.schema.json": jsfSpecVersion0_82, - "http://cyclonedx.org/schema/spdx.schema.json": spdxSpec, - "http://cyclonedx.org/schema/bom-1.5.schema.json": bomSpecVersion1_5, - "http://cyclonedx.org/schema/bom-1.6.schema.json": bomSpecVersion1_6, - "https://docs.oasis-open.org/csaf/csaf/v2.0/csaf_json_schema.json": casfSpecVersion2_0, - "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json": casfSpecVersion2_1, - "https://www.first.org/cvss/cvss-v2.0.json": cvssSpecVersion2_0, - "https://www.first.org/cvss/cvss-v3.0.json": cvssSpecVersion3_0, - "https://www.first.org/cvss/cvss-v3.1.json": cvssSpecVersion3_1, - "https://www.first.org/cvss/cvss-v4.0.json": cvssSpecVersion4_0, - "https://chainloop.dev/schemas/runner-context-response-0.1.schema.json": runnerContextSpecVersion0_1, - "https://schemas.chainloop.dev/prinfo/1.0/pr-info.schema.json": prInfoSpecVersion1_0, - "https://schemas.chainloop.dev/prinfo/1.1/pr-info.schema.json": prInfoSpecVersion1_1, - "https://schemas.chainloop.dev/prinfo/1.2/pr-info.schema.json": prInfoSpecVersion1_2, - "https://schemas.chainloop.dev/prinfo/1.3/pr-info.schema.json": prInfoSpecVersion1_3, - "https://schemas.chainloop.dev/aiagentconfig/0.1/ai-agent-config.schema.json": aiAgentConfigSpecVersion0_1, + "http://cyclonedx.org/schema/jsf-0.82.schema.json": jsfSpecVersion0_82, + "http://cyclonedx.org/schema/spdx.schema.json": spdxSpec, + "http://cyclonedx.org/schema/bom-1.5.schema.json": bomSpecVersion1_5, + "http://cyclonedx.org/schema/bom-1.6.schema.json": bomSpecVersion1_6, + "https://docs.oasis-open.org/csaf/csaf/v2.0/csaf_json_schema.json": casfSpecVersion2_0, + "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json": casfSpecVersion2_1, + "https://www.first.org/cvss/cvss-v2.0.json": cvssSpecVersion2_0, + "https://www.first.org/cvss/cvss-v3.0.json": cvssSpecVersion3_0, + "https://www.first.org/cvss/cvss-v3.1.json": cvssSpecVersion3_1, + "https://www.first.org/cvss/cvss-v4.0.json": cvssSpecVersion4_0, + "https://chainloop.dev/schemas/runner-context-response-0.1.schema.json": runnerContextSpecVersion0_1, + "https://schemas.chainloop.dev/prinfo/1.0/pr-info.schema.json": prInfoSpecVersion1_0, + "https://schemas.chainloop.dev/prinfo/1.1/pr-info.schema.json": prInfoSpecVersion1_1, + "https://schemas.chainloop.dev/prinfo/1.2/pr-info.schema.json": prInfoSpecVersion1_2, + "https://schemas.chainloop.dev/prinfo/1.3/pr-info.schema.json": prInfoSpecVersion1_3, + "https://schemas.chainloop.dev/aiagentconfig/0.1/ai-agent-config.schema.json": aiAgentConfigSpecVersion0_1, + "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json": aiCodingSessionSpecVersion0_1, } var compiledCycloneDxSchemas map[CycloneDXVersion]*jsonschema.Schema @@ -138,6 +148,7 @@ var compiledCSAFSchemas map[CSAFVersion]*jsonschema.Schema var compiledRunnerContextSchemas map[RunnerContextVersion]*jsonschema.Schema var compiledPRInfoSchemas map[PRInfoVersion]*jsonschema.Schema var compiledAIAgentConfigSchemas map[AIAgentConfigVersion]*jsonschema.Schema +var compiledAICodingSessionSchemas map[AICodingSessionVersion]*jsonschema.Schema func init() { compiler := jsonschema.NewCompiler() @@ -164,6 +175,9 @@ func init() { compiledAIAgentConfigSchemas = make(map[AIAgentConfigVersion]*jsonschema.Schema) compiledAIAgentConfigSchemas[AIAgentConfigVersion0_1] = compiler.MustCompile("https://schemas.chainloop.dev/aiagentconfig/0.1/ai-agent-config.schema.json") + + compiledAICodingSessionSchemas = make(map[AICodingSessionVersion]*jsonschema.Schema) + compiledAICodingSessionSchemas[AICodingSessionVersion0_1] = compiler.MustCompile("https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json") } // ValidateCycloneDX validates the given object against the specified CycloneDX schema version. @@ -299,6 +313,28 @@ func ValidateAIAgentConfig(data any, version AIAgentConfigVersion) error { return nil } +// ValidateAICodingSession validates the AI coding session schema. +func ValidateAICodingSession(data any, version AICodingSessionVersion) error { + if version == "" { + version = AICodingSessionVersion0_1 + } + + schema, ok := compiledAICodingSessionSchemas[version] + if !ok { + return errors.New("invalid AI coding session schema version") + } + + if err := schema.Validate(data); err != nil { + var invalidJSONTypeError jsonschema.InvalidJSONTypeError + if errors.As(err, &invalidJSONTypeError) { + return ErrInvalidJSONPayload + } + return err + } + + return nil +} + // errorIsJSONFormat checks if the error is a JSON format error. func errorIsJSONFormat(err error) error { var invalidJSONTypeError jsonschema.InvalidJSONTypeError diff --git a/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go b/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go new file mode 100644 index 000000000..c0c5f7f05 --- /dev/null +++ b/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go @@ -0,0 +1,102 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package materials + +import ( + "context" + "encoding/json" + "fmt" + "os" + + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/internal/aicodingsession" + "github.com/chainloop-dev/chainloop/internal/schemavalidators" + api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/casclient" + + "github.com/rs/zerolog" +) + +var annotationAICodingModel = api.CreateAnnotation("material.aicoding.model") + +type ChainloopAICodingSessionCrafter struct { + *crafterCommon + backend *casclient.CASBackend +} + +// NewChainloopAICodingSessionCrafter generates a new CHAINLOOP_AI_CODING_SESSION material. +// This material type contains AI coding session telemetry collected during attestation. +func NewChainloopAICodingSessionCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*ChainloopAICodingSessionCrafter, error) { + if schema.Type != schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION { + return nil, fmt.Errorf("material type is not chainloop_ai_coding_session") + } + + craftCommon := &crafterCommon{logger: l, input: schema} + return &ChainloopAICodingSessionCrafter{backend: backend, crafterCommon: craftCommon}, nil +} + +// Craft validates the AI coding session against the JSON schema, calculates the digest, +// uploads it and returns the material definition. +func (c *ChainloopAICodingSessionCrafter) Craft(ctx context.Context, artifactPath string) (*api.Attestation_Material, error) { + f, err := os.ReadFile(artifactPath) + if err != nil { + return nil, fmt.Errorf("can't open the file: %w", err) + } + + // Unmarshal envelope, keeping data as raw JSON for schema validation + var envelope struct { + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal(f, &envelope); err != nil { + c.logger.Debug().Err(err).Msg("error decoding file") + return nil, fmt.Errorf("invalid JSON format: %w", err) + } + + // Unmarshal data into typed struct for annotation extraction + var data aicodingsession.Data + if err := json.Unmarshal(envelope.Data, &data); err != nil { + c.logger.Debug().Err(err).Msg("error decoding data field") + return nil, fmt.Errorf("failed to unmarshal data: %w", err) + } + + // Validate using raw JSON to preserve unknown fields for strict schema validation + var rawData any + if err := json.Unmarshal(envelope.Data, &rawData); err != nil { + return nil, fmt.Errorf("failed to unmarshal data for validation: %w", err) + } + + if err := schemavalidators.ValidateAICodingSession(rawData, schemavalidators.AICodingSessionVersion0_1); err != nil { + c.logger.Debug().Err(err).Msg("schema validation failed") + return nil, fmt.Errorf("AI coding session validation failed: %w", err) + } + + material, err := uploadAndCraft(ctx, c.input, c.backend, artifactPath, c.logger) + if err != nil { + return nil, err + } + + // Surface agent name as an annotation + if data.Agent.Name != "" { + material.Annotations[annotationAIAgentName] = data.Agent.Name + } + + // Surface primary model as an annotation + if data.Model != nil && data.Model.Primary != "" { + material.Annotations[annotationAICodingModel] = data.Model.Primary + } + + return material, nil +} diff --git a/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go b/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go new file mode 100644 index 000000000..dd6b4bd22 --- /dev/null +++ b/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go @@ -0,0 +1,284 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package materials + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/internal/aicodingsession" + "github.com/chainloop-dev/chainloop/internal/schemavalidators" + "github.com/chainloop-dev/chainloop/pkg/casclient" + mUploader "github.com/chainloop-dev/chainloop/pkg/casclient/mocks" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewChainloopAICodingSessionCrafter_WrongType(t *testing.T) { + logger := zerolog.Nop() + + schema := &schemaapi.CraftingSchema_Material{ + Type: schemaapi.CraftingSchema_Material_SBOM_CYCLONEDX_JSON, + } + + _, err := NewChainloopAICodingSessionCrafter(schema, nil, &logger) + require.Error(t, err) + assert.Contains(t, err.Error(), "material type is not chainloop_ai_coding_session") +} + +func TestNewChainloopAICodingSessionCrafter_CorrectType(t *testing.T) { + logger := zerolog.Nop() + + schema := &schemaapi.CraftingSchema_Material{ + Type: schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION, + } + + crafter, err := NewChainloopAICodingSessionCrafter(schema, nil, &logger) + require.NoError(t, err) + assert.NotNil(t, crafter) +} + +func TestChainloopAICodingSessionCrafter_Validation(t *testing.T) { + testCases := []struct { + name string + data *aicodingsession.Data + wantErr bool + }{ + { + name: "valid full session", + data: &aicodingsession.Data{ + SchemaVersion: "v1", + Agent: aicodingsession.Agent{Name: "claude-code", Version: "2.1.83"}, + Session: aicodingsession.Session{ + ID: "fa8acbe6-a176-4c2a-b51e-fd4541615eb5", + Slug: "stateful-wobbling-sutherland", + StartedAt: "2026-03-25T15:10:49.161Z", + EndedAt: "2026-03-25T16:59:14.988Z", + DurationSeconds: 6505, + }, + GitContext: &aicodingsession.GitContext{ + Repository: "git@github.com:example/repo.git", + Branch: "main", + }, + Model: &aicodingsession.Model{ + Primary: "claude-opus-4-6", + Provider: "anthropic", + }, + Usage: &aicodingsession.Usage{ + TotalTokens: 3052, + EstimatedCostUSD: 0.84, + }, + }, + wantErr: false, + }, + { + name: "valid minimal session", + data: &aicodingsession.Data{ + SchemaVersion: "v1", + Agent: aicodingsession.Agent{Name: "cursor"}, + Session: aicodingsession.Session{ + ID: "abc-123", + StartedAt: "2026-03-25T15:10:49.161Z", + DurationSeconds: 100, + }, + }, + wantErr: false, + }, + { + name: "missing required fields", + data: &aicodingsession.Data{}, + wantErr: true, + }, + { + name: "missing session", + data: &aicodingsession.Data{ + SchemaVersion: "v1", + Agent: aicodingsession.Agent{Name: "claude-code"}, + }, + wantErr: true, + }, + { + name: "missing agent name", + data: &aicodingsession.Data{ + SchemaVersion: "v1", + Agent: aicodingsession.Agent{}, + Session: aicodingsession.Session{ + ID: "abc", + StartedAt: "2026-03-25T15:10:49.161Z", + DurationSeconds: 100, + }, + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dataBytes, err := json.Marshal(tc.data) + require.NoError(t, err) + + var rawData any + require.NoError(t, json.Unmarshal(dataBytes, &rawData)) + + err = schemavalidators.ValidateAICodingSession(rawData, schemavalidators.AICodingSessionVersion0_1) + + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestChainloopAICodingSessionCrafter_InvalidJSON(t *testing.T) { + logger := zerolog.Nop() + schema := &schemaapi.CraftingSchema_Material{ + Type: schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION, + } + + crafter, err := NewChainloopAICodingSessionCrafter(schema, nil, &logger) + require.NoError(t, err) + + tmpFile := filepath.Join(t.TempDir(), "invalid.json") + require.NoError(t, os.WriteFile(tmpFile, []byte(`{invalid json}`), 0o600)) + + _, err = crafter.Craft(context.Background(), tmpFile) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid JSON format") +} + +func TestChainloopAICodingSessionCrafter_InvalidSchema(t *testing.T) { + logger := zerolog.Nop() + schema := &schemaapi.CraftingSchema_Material{ + Type: schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION, + } + + crafter, err := NewChainloopAICodingSessionCrafter(schema, nil, &logger) + require.NoError(t, err) + + // Valid envelope but data is missing required fields + tmpFile := filepath.Join(t.TempDir(), "bad-schema.json") + require.NoError(t, os.WriteFile(tmpFile, []byte(`{"chainloop.material.evidence.id":"CHAINLOOP_AI_CODING_SESSION","schema":"test","data":{"foo":"bar"}}`), 0o600)) + + _, err = crafter.Craft(context.Background(), tmpFile) + require.Error(t, err) + assert.Contains(t, err.Error(), "AI coding session validation failed") +} + +func TestChainloopAICodingSessionCrafter_FileNotFound(t *testing.T) { + logger := zerolog.Nop() + schema := &schemaapi.CraftingSchema_Material{ + Type: schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION, + } + + crafter, err := NewChainloopAICodingSessionCrafter(schema, nil, &logger) + require.NoError(t, err) + + _, err = crafter.Craft(context.Background(), "/nonexistent/file.json") + require.Error(t, err) + assert.Contains(t, err.Error(), "can't open the file") +} + +func TestChainloopAICodingSessionCrafter_RejectsExtraFields(t *testing.T) { + logger := zerolog.Nop() + schema := &schemaapi.CraftingSchema_Material{ + Type: schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION, + } + + crafter, err := NewChainloopAICodingSessionCrafter(schema, nil, &logger) + require.NoError(t, err) + + payload := `{ + "chainloop.material.evidence.id": "CHAINLOOP_AI_CODING_SESSION", + "schema": "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", + "data": { + "schema_version": "v1", + "agent": {"name": "claude-code"}, + "session": {"id": "abc", "started_at": "2026-03-25T15:10:49.161Z", "duration_seconds": 100}, + "unknown_field": "should fail" + } + }` + + tmpFile := filepath.Join(t.TempDir(), "extra-fields.json") + require.NoError(t, os.WriteFile(tmpFile, []byte(payload), 0o600)) + + _, err = crafter.Craft(context.Background(), tmpFile) + require.Error(t, err) + assert.Contains(t, err.Error(), "AI coding session validation failed") +} + +func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) { + testCases := []struct { + name string + filePath string + expectedAgentName string + expectedModel string + modelPresent bool + }{ + { + name: "full session with model", + filePath: "./testdata/ai-coding-session.json", + expectedAgentName: "claude-code", + expectedModel: "claude-opus-4-6", + modelPresent: true, + }, + { + name: "minimal session without model", + filePath: "./testdata/ai-coding-session-minimal.json", + expectedAgentName: "cursor", + modelPresent: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + logger := zerolog.Nop() + schema := &schemaapi.CraftingSchema_Material{ + Name: "test", + Type: schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION, + } + + uploader := mUploader.NewUploader(t) + uploader.On("UploadFile", context.TODO(), tc.filePath). + Return(&casclient.UpDownStatus{ + Digest: "deadbeef", + Filename: tc.filePath, + }, nil) + + backend := &casclient.CASBackend{Uploader: uploader} + + crafter, err := NewChainloopAICodingSessionCrafter(schema, backend, &logger) + require.NoError(t, err) + + got, err := crafter.Craft(context.TODO(), tc.filePath) + require.NoError(t, err) + + assert.Equal(t, tc.expectedAgentName, got.Annotations[annotationAIAgentName]) + if tc.modelPresent { + assert.Equal(t, tc.expectedModel, got.Annotations[annotationAICodingModel]) + } else { + _, exists := got.Annotations[annotationAICodingModel] + assert.False(t, exists, "model annotation should not be present") + } + }) + } +} diff --git a/pkg/attestation/crafter/materials/materials.go b/pkg/attestation/crafter/materials/materials.go index 9d2ed3b7f..6842efffd 100644 --- a/pkg/attestation/crafter/materials/materials.go +++ b/pkg/attestation/crafter/materials/materials.go @@ -291,6 +291,8 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia crafter, err = NewGitleaksReportCrafter(materialSchema, casBackend, logger) case schemaapi.CraftingSchema_Material_CHAINLOOP_AI_AGENT_CONFIG: crafter, err = NewChainloopAIAgentConfigCrafter(materialSchema, casBackend, logger) + case schemaapi.CraftingSchema_Material_CHAINLOOP_AI_CODING_SESSION: + crafter, err = NewChainloopAICodingSessionCrafter(materialSchema, casBackend, logger) default: return nil, fmt.Errorf("material of type %q not supported yet", materialSchema.Type) } diff --git a/pkg/attestation/crafter/materials/testdata/ai-coding-session-minimal.json b/pkg/attestation/crafter/materials/testdata/ai-coding-session-minimal.json new file mode 100644 index 000000000..526ba453b --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/ai-coding-session-minimal.json @@ -0,0 +1,15 @@ +{ + "chainloop.material.evidence.id": "CHAINLOOP_AI_CODING_SESSION", + "schema": "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", + "data": { + "schema_version": "v1", + "agent": { + "name": "cursor" + }, + "session": { + "id": "abc-123", + "started_at": "2026-03-25T15:10:49.161Z", + "duration_seconds": 100 + } + } +} diff --git a/pkg/attestation/crafter/materials/testdata/ai-coding-session.json b/pkg/attestation/crafter/materials/testdata/ai-coding-session.json new file mode 100644 index 000000000..09f06f47c --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/ai-coding-session.json @@ -0,0 +1,77 @@ +{ + "chainloop.material.evidence.id": "CHAINLOOP_AI_CODING_SESSION", + "schema": "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", + "data": { + "schema_version": "v1", + "agent": { + "name": "claude-code", + "version": "2.1.83" + }, + "session": { + "id": "fa8acbe6-a176-4c2a-b51e-fd4541615eb5", + "slug": "stateful-wobbling-sutherland", + "started_at": "2026-03-25T15:10:49.161Z", + "ended_at": "2026-03-25T16:59:14.988Z", + "duration_seconds": 6505 + }, + "git_context": { + "repository": "git@github.com:example/repo.git", + "branch": "main", + "work_dir": "/home/user/repo", + "commit_start": "ae79df1aae6ef53fea636b4aad0a8c3d372178e9", + "commit_end": "5869b91e80fea5ac3d4502c728b118e85d65d825", + "commits": [ + "ae79df1 Add Go hello world application", + "5869b91 Add --bye flag" + ], + "commit_count": 2 + }, + "code_changes": { + "files_modified": 0, + "files_created": 2, + "files_deleted": 0, + "lines_added": 36, + "lines_removed": 0, + "files": [ + {"path": "go.mod", "status": "created"}, + {"path": "main.go", "status": "created"} + ] + }, + "model": { + "primary": "claude-opus-4-6", + "provider": "anthropic", + "models_used": ["claude-opus-4-6"] + }, + "usage": { + "input_tokens": 63, + "output_tokens": 2989, + "total_tokens": 3052, + "cache_read_input_tokens": 933233, + "cache_creation_input_tokens": 47538, + "estimated_cost_usd": 0.8388 + }, + "tools_used": { + "summary": [ + {"tool_name": "Bash", "invocation_count": 13}, + {"tool_name": "Edit", "invocation_count": 4} + ], + "total_invocations": 17 + }, + "conversation": { + "total_messages": 87, + "user_messages": 34, + "assistant_messages": 36 + }, + "subagents": [], + "raw_session": { + "main": [ + { + "type": "user", + "message": {"role": "user", "content": "hello world"}, + "timestamp": "2026-03-25T15:13:37.330Z" + } + ] + }, + "warnings": [] + } +} From 535fbff631a79b903e3ee25ca343d5a7d281c99a Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 25 Mar 2026 21:10:01 +0100 Subject: [PATCH 2/5] generate cli docs Signed-off-by: Jose I. Paris --- app/cli/documentation/cli-reference.mdx | 4 +-- .../prinfo/pr-info-1.2.schema.json | 26 ++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 7e3a993cd..83ac17fcd 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -246,7 +246,7 @@ Options --annotation strings additional annotation in the format of key=value --attestation-id string Unique identifier of the in-progress attestation -h, --help help for add ---kind string kind of the material to be recorded: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"] +--kind string kind of the material to be recorded: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"] --name string name of the material as shown in the contract --no-strict-validation skip strict schema validation for SBOM_CYCLONEDX_JSON materials --registry-password string registry password, ($CHAINLOOP_REGISTRY_PASSWORD) @@ -2934,7 +2934,7 @@ Options --annotation strings Key-value pairs of material annotations (key=value) -h, --help help for eval --input stringArray Key-value pairs of policy inputs (key=value) ---kind string Kind of the material: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"] +--kind string Kind of the material: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"] --material string Path to material or attestation file -p, --policy string Policy reference (./my-policy.yaml, https://my-domain.com/my-policy.yaml, chainloop://my-stored-policy) (default "policy.yaml") ``` diff --git a/internal/schemavalidators/internal_schemas/prinfo/pr-info-1.2.schema.json b/internal/schemavalidators/internal_schemas/prinfo/pr-info-1.2.schema.json index 96e31b1ee..a7238db51 100644 --- a/internal/schemavalidators/internal_schemas/prinfo/pr-info-1.2.schema.json +++ b/internal/schemavalidators/internal_schemas/prinfo/pr-info-1.2.schema.json @@ -44,8 +44,28 @@ "description": "Direct URL to the PR/MR" }, "author": { - "type": "string", - "description": "Username of the PR/MR author" + "properties": { + "login": { + "type": "string", + "description": "Username of the PR/MR author" + }, + "type": { + "type": "string", + "enum": [ + "User", + "Bot", + "unknown" + ], + "description": "Account type of the PR/MR author" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "login", + "type" + ], + "description": "The PR/MR author" }, "reviewers": { "items": { @@ -101,4 +121,4 @@ ], "title": "Pull Request / Merge Request Information", "description": "Schema for Pull Request or Merge Request metadata collected during attestation" -} +} \ No newline at end of file From e23dcb11eea28177fc901f2aef4a30223573fbb3 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Mar 2026 00:25:09 +0100 Subject: [PATCH 3/5] fix: rename ai coding session model annotation to match aiagent pattern Rename material.aicoding.model to material.aiagent.model for consistency with the existing material.aiagent.name annotation. Signed-off-by: Jose I. Paris --- .../crafter/materials/chainloop_ai_coding_session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go b/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go index c0c5f7f05..c8aa7ad76 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go @@ -30,7 +30,7 @@ import ( "github.com/rs/zerolog" ) -var annotationAICodingModel = api.CreateAnnotation("material.aicoding.model") +var annotationAICodingModel = api.CreateAnnotation("material.aiagent.model") type ChainloopAICodingSessionCrafter struct { *crafterCommon From 165659a434f4209d6fff89e3f1d88b4941083905 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Mar 2026 08:44:22 +0100 Subject: [PATCH 4/5] feat: add schema annotation to AI agent config and coding session materials Extract the envelope schema field as a chainloop.material.evidence.schema annotation, consistent with the evidence material type. Signed-off-by: Jose I. Paris --- .../crafter/materials/chainloop_ai_agent_config.go | 8 +++++++- .../crafter/materials/chainloop_ai_agent_config_test.go | 4 ++++ .../crafter/materials/chainloop_ai_coding_session.go | 8 +++++++- .../crafter/materials/chainloop_ai_coding_session_test.go | 4 ++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pkg/attestation/crafter/materials/chainloop_ai_agent_config.go b/pkg/attestation/crafter/materials/chainloop_ai_agent_config.go index a89e1e03b..8e5055ea1 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_agent_config.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_agent_config.go @@ -58,7 +58,8 @@ func (c *ChainloopAIAgentConfigCrafter) Craft(ctx context.Context, artifactPath // Unmarshal envelope, keeping data as raw JSON for schema validation var envelope struct { - Data json.RawMessage `json:"data"` + Schema string `json:"schema"` + Data json.RawMessage `json:"data"` } if err := json.Unmarshal(f, &envelope); err != nil { c.logger.Debug().Err(err).Msg("error decoding file") @@ -88,6 +89,11 @@ func (c *ChainloopAIAgentConfigCrafter) Craft(ctx context.Context, artifactPath return nil, err } + // Surface schema as an annotation + if envelope.Schema != "" { + material.Annotations[annotationEvidenceSchema] = envelope.Schema + } + // Surface agent name as an annotation if data.Agent.Name != "" { material.Annotations[annotationAIAgentName] = data.Agent.Name 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 877fdd9db..fdd677b78 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_agent_config_test.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_agent_config_test.go @@ -266,16 +266,19 @@ func TestChainloopAIAgentConfigCrafter_AgentNameAnnotation(t *testing.T) { name string filePath string expectedAgentName string + expectedSchema string }{ { name: "claude agent", filePath: "./testdata/ai-agent-config-claude.json", expectedAgentName: "claude", + expectedSchema: "https://schemas.chainloop.dev/aiagentconfig/0.1/ai-agent-config.schema.json", }, { name: "cursor agent", filePath: "./testdata/ai-agent-config-cursor.json", expectedAgentName: "cursor", + expectedSchema: "https://schemas.chainloop.dev/aiagentconfig/0.1/ai-agent-config.schema.json", }, } @@ -303,6 +306,7 @@ func TestChainloopAIAgentConfigCrafter_AgentNameAnnotation(t *testing.T) { require.NoError(t, err) assert.Equal(t, tc.expectedAgentName, got.Annotations[annotationAIAgentName]) + assert.Equal(t, tc.expectedSchema, got.Annotations[annotationEvidenceSchema]) }) } } diff --git a/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go b/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go index c8aa7ad76..2630b207e 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go @@ -58,7 +58,8 @@ func (c *ChainloopAICodingSessionCrafter) Craft(ctx context.Context, artifactPat // Unmarshal envelope, keeping data as raw JSON for schema validation var envelope struct { - Data json.RawMessage `json:"data"` + Schema string `json:"schema"` + Data json.RawMessage `json:"data"` } if err := json.Unmarshal(f, &envelope); err != nil { c.logger.Debug().Err(err).Msg("error decoding file") @@ -88,6 +89,11 @@ func (c *ChainloopAICodingSessionCrafter) Craft(ctx context.Context, artifactPat return nil, err } + // Surface schema as an annotation + if envelope.Schema != "" { + material.Annotations[annotationEvidenceSchema] = envelope.Schema + } + // Surface agent name as an annotation if data.Agent.Name != "" { material.Annotations[annotationAIAgentName] = data.Agent.Name diff --git a/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go b/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go index dd6b4bd22..dabe4839d 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go @@ -232,6 +232,7 @@ func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) { filePath string expectedAgentName string expectedModel string + expectedSchema string modelPresent bool }{ { @@ -239,12 +240,14 @@ func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) { filePath: "./testdata/ai-coding-session.json", expectedAgentName: "claude-code", expectedModel: "claude-opus-4-6", + expectedSchema: "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", modelPresent: true, }, { name: "minimal session without model", filePath: "./testdata/ai-coding-session-minimal.json", expectedAgentName: "cursor", + expectedSchema: "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", modelPresent: false, }, } @@ -273,6 +276,7 @@ func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) { require.NoError(t, err) assert.Equal(t, tc.expectedAgentName, got.Annotations[annotationAIAgentName]) + assert.Equal(t, tc.expectedSchema, got.Annotations[annotationEvidenceSchema]) if tc.modelPresent { assert.Equal(t, tc.expectedModel, got.Annotations[annotationAICodingModel]) } else { From 6ebc120c45605065a61cc9ea76972e3fd9df8a3d Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Mar 2026 09:16:36 +0100 Subject: [PATCH 5/5] move package Signed-off-by: Jose I. Paris --- .../materials}/aicodingsession/aicodingsession.go | 0 .../crafter/materials/chainloop_ai_agent_config.go | 8 +------- .../materials/chainloop_ai_agent_config_test.go | 4 ---- .../crafter/materials/chainloop_ai_coding_session.go | 10 ++-------- .../materials/chainloop_ai_coding_session_test.go | 6 +----- 5 files changed, 4 insertions(+), 24 deletions(-) rename {internal => pkg/attestation/crafter/materials}/aicodingsession/aicodingsession.go (100%) diff --git a/internal/aicodingsession/aicodingsession.go b/pkg/attestation/crafter/materials/aicodingsession/aicodingsession.go similarity index 100% rename from internal/aicodingsession/aicodingsession.go rename to pkg/attestation/crafter/materials/aicodingsession/aicodingsession.go diff --git a/pkg/attestation/crafter/materials/chainloop_ai_agent_config.go b/pkg/attestation/crafter/materials/chainloop_ai_agent_config.go index 8e5055ea1..a89e1e03b 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_agent_config.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_agent_config.go @@ -58,8 +58,7 @@ func (c *ChainloopAIAgentConfigCrafter) Craft(ctx context.Context, artifactPath // Unmarshal envelope, keeping data as raw JSON for schema validation var envelope struct { - Schema string `json:"schema"` - Data json.RawMessage `json:"data"` + Data json.RawMessage `json:"data"` } if err := json.Unmarshal(f, &envelope); err != nil { c.logger.Debug().Err(err).Msg("error decoding file") @@ -89,11 +88,6 @@ func (c *ChainloopAIAgentConfigCrafter) Craft(ctx context.Context, artifactPath return nil, err } - // Surface schema as an annotation - if envelope.Schema != "" { - material.Annotations[annotationEvidenceSchema] = envelope.Schema - } - // Surface agent name as an annotation if data.Agent.Name != "" { material.Annotations[annotationAIAgentName] = data.Agent.Name 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 fdd677b78..877fdd9db 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_agent_config_test.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_agent_config_test.go @@ -266,19 +266,16 @@ func TestChainloopAIAgentConfigCrafter_AgentNameAnnotation(t *testing.T) { name string filePath string expectedAgentName string - expectedSchema string }{ { name: "claude agent", filePath: "./testdata/ai-agent-config-claude.json", expectedAgentName: "claude", - expectedSchema: "https://schemas.chainloop.dev/aiagentconfig/0.1/ai-agent-config.schema.json", }, { name: "cursor agent", filePath: "./testdata/ai-agent-config-cursor.json", expectedAgentName: "cursor", - expectedSchema: "https://schemas.chainloop.dev/aiagentconfig/0.1/ai-agent-config.schema.json", }, } @@ -306,7 +303,6 @@ func TestChainloopAIAgentConfigCrafter_AgentNameAnnotation(t *testing.T) { require.NoError(t, err) assert.Equal(t, tc.expectedAgentName, got.Annotations[annotationAIAgentName]) - assert.Equal(t, tc.expectedSchema, got.Annotations[annotationEvidenceSchema]) }) } } diff --git a/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go b/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go index 2630b207e..1192c238a 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_coding_session.go @@ -22,9 +22,9 @@ import ( "os" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" - "github.com/chainloop-dev/chainloop/internal/aicodingsession" "github.com/chainloop-dev/chainloop/internal/schemavalidators" api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/aicodingsession" "github.com/chainloop-dev/chainloop/pkg/casclient" "github.com/rs/zerolog" @@ -58,8 +58,7 @@ func (c *ChainloopAICodingSessionCrafter) Craft(ctx context.Context, artifactPat // Unmarshal envelope, keeping data as raw JSON for schema validation var envelope struct { - Schema string `json:"schema"` - Data json.RawMessage `json:"data"` + Data json.RawMessage `json:"data"` } if err := json.Unmarshal(f, &envelope); err != nil { c.logger.Debug().Err(err).Msg("error decoding file") @@ -89,11 +88,6 @@ func (c *ChainloopAICodingSessionCrafter) Craft(ctx context.Context, artifactPat return nil, err } - // Surface schema as an annotation - if envelope.Schema != "" { - material.Annotations[annotationEvidenceSchema] = envelope.Schema - } - // Surface agent name as an annotation if data.Agent.Name != "" { material.Annotations[annotationAIAgentName] = data.Agent.Name diff --git a/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go b/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go index dabe4839d..144086d49 100644 --- a/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go +++ b/pkg/attestation/crafter/materials/chainloop_ai_coding_session_test.go @@ -23,8 +23,8 @@ import ( "testing" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" - "github.com/chainloop-dev/chainloop/internal/aicodingsession" "github.com/chainloop-dev/chainloop/internal/schemavalidators" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/aicodingsession" "github.com/chainloop-dev/chainloop/pkg/casclient" mUploader "github.com/chainloop-dev/chainloop/pkg/casclient/mocks" "github.com/rs/zerolog" @@ -232,7 +232,6 @@ func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) { filePath string expectedAgentName string expectedModel string - expectedSchema string modelPresent bool }{ { @@ -240,14 +239,12 @@ func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) { filePath: "./testdata/ai-coding-session.json", expectedAgentName: "claude-code", expectedModel: "claude-opus-4-6", - expectedSchema: "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", modelPresent: true, }, { name: "minimal session without model", filePath: "./testdata/ai-coding-session-minimal.json", expectedAgentName: "cursor", - expectedSchema: "https://schemas.chainloop.dev/aicodingsession/0.1/ai-coding-session.schema.json", modelPresent: false, }, } @@ -276,7 +273,6 @@ func TestChainloopAICodingSessionCrafter_Annotations(t *testing.T) { require.NoError(t, err) assert.Equal(t, tc.expectedAgentName, got.Annotations[annotationAIAgentName]) - assert.Equal(t, tc.expectedSchema, got.Annotations[annotationEvidenceSchema]) if tc.modelPresent { assert.Equal(t, tc.expectedModel, got.Annotations[annotationAICodingModel]) } else {