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/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/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/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 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/aicodingsession/aicodingsession.go b/pkg/attestation/crafter/materials/aicodingsession/aicodingsession.go new file mode 100644 index 000000000..f614668d7 --- /dev/null +++ b/pkg/attestation/crafter/materials/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/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..1192c238a --- /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/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" +) + +var annotationAICodingModel = api.CreateAnnotation("material.aiagent.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..144086d49 --- /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/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" + "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": [] + } +}